mongoid-ids 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f98fcca5a53dd0256f8e74ab38719ce9bd2e001b
4
+ data.tar.gz: 47b8832b2ff1d6a217aac882b275770ff9c7fb24
5
+ SHA512:
6
+ metadata.gz: 37852de0e84195e089881f026e322679977bfc3a576d5bfd57dcd14e97c6983110875ee5896a7b318509d5a983d6e07a22f4f08ae7edfc99808c0950cbc18579
7
+ data.tar.gz: cc4fa090eabec9cd29b999e04406d680e73537e2bec545d28b0f115665c2f40a0f782ed5aa016a1def94872e04953ceac65c5ca3e6bedcd780b55ecda59eb952
data/.autotest ADDED
File without changes
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --tty
2
+ --color
3
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,18 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1.0
7
+ - 2.1.1
8
+ - 2.2.0
9
+
10
+ gemfile:
11
+ - Gemfile
12
+
13
+ services:
14
+ - mongodb
15
+
16
+ env: CI="travis"
17
+
18
+ script: "bundle exec rspec"
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'rspec', '~> 3.1.0'
7
+ gem "codeclimate-test-reporter", require: nil
8
+ gem 'database_cleaner'
9
+ gem 'mongoid-rspec', '~> 2.0.0'
10
+ end
data/Guardfile ADDED
@@ -0,0 +1,22 @@
1
+ # Note: The cmd option is now required due to the increasing number of ways
2
+ # rspec may be run, below are examples of the most common uses.
3
+ # * bundler: 'bundle exec rspec'
4
+ # * bundler binstubs: 'bin/rspec'
5
+ # * spring: 'bin/rsspec' (This will use spring if running and you have
6
+ # installed the spring binstubs per the docs)
7
+ # * zeus: 'zeus rspec' (requires the server to be started separetly)
8
+ # * 'just' rspec: 'rspec'
9
+
10
+ # guard :rubocop do
11
+ guard :rubocop, all_on_start: false, keep_failed: false, cli: ['-D'] do
12
+ watch(%r{.+\.rb$})
13
+ watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
14
+ end
15
+
16
+ guard :rspec, cmd: 'bundle exec rspec' do
17
+ watch(%r{^spec/.+_spec\.rb$})
18
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
19
+ watch(/^generators\/(.+)\.rb$/) { |_m| 'spec/schemaless/worker_spec' }
20
+
21
+ watch('spec/spec_helper.rb') { 'spec' }
22
+ end
data/MIT-LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Nicholas Bruning
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,225 @@
1
+ # Mongoid::Ids - Short snappy ids/tokens for Mongoid documents
2
+
3
+ [![Build Status](https://secure.travis-ci.org/nofxx/mongoid-ids.png)](http://travis-ci.org/nofxx/mongoid-ids)
4
+ [![Code Climate](https://codeclimate.com/github/nofxx/mongoid-ids.png)](https://codeclimate.com/github/nofxx/mongoid-ids)
5
+
6
+ This library is a quick and simple way to generate unique, random tokens
7
+ for your mongoid documents, in the cases where you can't, or don't want
8
+ to use slugs, or the default MongoDB ObjectIDs.
9
+
10
+ Mongoid::Ids can help turn this:
11
+
12
+ http://bestappever.com/video/4dcfbb3c6a4f1d4c4a000012
13
+
14
+ Into something more like this:
15
+
16
+ http://bestappever.com/video/8tmQ9p
17
+
18
+
19
+ ## Getting started
20
+
21
+ In your gemfile, add:
22
+
23
+ gem 'mongoid-ids', '~> 2.0.0'
24
+
25
+ Then update your bundle
26
+
27
+ $ bundle update
28
+
29
+ In your Mongoid documents, just add `include Mongoid::Ids` and the
30
+ `token` method will take care of all the setup, like so:
31
+
32
+ ```ruby
33
+ class Person
34
+ include Mongoid::Document
35
+ include Mongoid::Ids
36
+
37
+ field :name
38
+
39
+ token
40
+ end
41
+
42
+ ```
43
+
44
+ And that's it! There's lots of configuration options too - which are all
45
+ listed [below](#configuration). By default, the `token` method will
46
+ create tokens 4 characters long, containing random alphanumeric characters.
47
+
48
+ __Note:__ Mongoid::Ids leverages Mongoid's 'safe mode' by
49
+ automatically creating a unique index on your documents using the token
50
+ field. In order to take advantage of this feature (and ensure that your
51
+ documents always have unique tokens) remember to create your indexes.
52
+
53
+
54
+ ## Finders
55
+
56
+ By default, the gem will override the existing `find` method in Mongoid,
57
+ in order to search for documents based on their token first (although
58
+ the default behaviour of ObjectIDs is also there). You can disable these
59
+ with the [`skip_finders` configuration option](#skip-finders-skip_finders).
60
+
61
+ ```ruby
62
+ Video.find("x3v98")
63
+ Account.find("ACC-123456")
64
+ ```
65
+
66
+
67
+ ## Configuration
68
+
69
+ ### Idss
70
+
71
+ As of `Mongoid::Ids` 2.0.0, you can now choose between two different
72
+ systems for managing how your tokens look.
73
+
74
+ For simple setup, you can use
75
+ combination of the [`length`](#length-length) and [`contains`](#contains-contains), which modify the length and
76
+ types of characters to use.
77
+
78
+ For when you need to generate more complex tokens, you can use the
79
+ [`pattern`](#patterns-pattern) option, which allows for very low-level control of the precise
80
+ structure of your tokens, as well as allowing for static strings, like
81
+ prefixes, infixes or suffixes.
82
+
83
+ #### Length (`:length`)
84
+
85
+ This one is easy, it's just an integer.
86
+
87
+ __Example:__
88
+
89
+ ```ruby
90
+ token :length => 6 # Tokens are now of length 6
91
+ token :length => 12 # Whow, whow, whow. Slow down egghead.
92
+ ```
93
+
94
+ You get the idea.
95
+
96
+ The only caveat here is that if used in combination with the
97
+ `:contains => :numeric` option, tokens may vary in length _up to_ the
98
+ specified length.
99
+
100
+ #### Contains (`:contains`)
101
+
102
+ Contains has 7 different options:
103
+
104
+ * `:alphanumeric` - contains uppercase & lowercase characters, as well
105
+ as numbers
106
+ * `:alpha` - contains only uppercase & lowercase characters
107
+ * `:alpha_upper` - contains only uppercase letters
108
+ * `:alpha_lower` - contains only lowercase letters
109
+ * `:numeric` - integer, length may be shorter than `:length`
110
+ * `:fixed_numeric` - integer, but will always be of length `:length`
111
+ * `:fixed_numeric_no_leading_zeros` - same as `:fixed_numeric`, but will
112
+ never start with zeros
113
+
114
+ __Examples:__
115
+ ```ruby
116
+ token :contains => :alpha_upper, :length => 8
117
+ token :contains => :fixed_numeric
118
+ ```
119
+
120
+ #### Patterns (`:pattern`)
121
+
122
+ New in 2.0.0, patterns allow you fine-grained control over how your
123
+ tokens look. It's great for generating random data that has a
124
+ requirements to also have some basic structure. If you use the
125
+ `:pattern` option, it will override both the `:length` and `:contains`
126
+ options.
127
+
128
+ This was designed to operate in a similar way to something like `strftime`,
129
+ if the syntax offends you - please open an issue, I'd love to get some
130
+ feedback here and better refine how these are generated.
131
+
132
+ Any characters in the string are treated as static, except those that are
133
+ proceeded by a `%`. Those special characters represent a single, randomly
134
+ generated character, and are as follows:
135
+
136
+ * `%s` - any uppercase, lowercase, or numeric character
137
+ * `%w` - any uppercase, or lowercase character
138
+ * `%c` - any lowercase character
139
+ * `%C` - any uppercase character
140
+ * `%d` - any digit
141
+ * `%D` - any non-zero digit
142
+
143
+ __Example:__
144
+
145
+ ```ruby
146
+ token :pattern => "PRE-%C%C-%d%d%d%d" # Generates something like: 'PRE-ND-3485'
147
+ ```
148
+
149
+ You can also add a repetition modifier, which can help improve readability on
150
+ more complex patterns. You simply add any integer after the letter.
151
+
152
+ __Examples:__
153
+
154
+ ```ruby
155
+ token :pattern => "APP-%d6" # Generates something like; "APP-638924"
156
+ ```
157
+
158
+ ### Field Name
159
+
160
+ This allows you to change the field name used by `Mongoid::Ids`
161
+ This is particularly handy to use multiple tokens one a single document.
162
+
163
+ __Examples:__
164
+ ```ruby
165
+ token :length => 6
166
+ token :sharing_token, :length => 12
167
+ token :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
+ ```ruby
178
+ token :skip_finders => true
179
+ ```
180
+
181
+
182
+ ### Override to_param (`:override_to_param`)
183
+
184
+ By default, `Mongoid::Ids` 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
+ ```ruby
190
+ token :override_to_param => false
191
+ ```
192
+
193
+
194
+ ### Retry Count (`:retry_count`)
195
+
196
+ In the event of a token collision, this gem will attempt to try three
197
+ more times before raising a `Mongoid::Ids::CollisionRetriesExceeded`
198
+ error. If you're wanting it to try harder, or less hard, then this
199
+ option is for you.
200
+
201
+ __Examples:__
202
+ ```ruby
203
+ token :retry_count => 9
204
+ token :retry_count => 0
205
+ ```
206
+
207
+ # Notes
208
+
209
+ If you find a problem, please [submit an issue](http://github.com/nofxx/mongoid-ids/issues) (and a failing test, if
210
+ you can). Pull requests and feature requests are always welcome and
211
+ greatly appreciated.
212
+
213
+ Thanks to everyone that has contributed to this gem over the past year.
214
+ Many, many thanks - you guys rawk.
215
+
216
+
217
+ ## Contributors:
218
+
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).
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,48 @@
1
+ $: << File.expand_path("../../lib", __FILE__)
2
+
3
+ require 'database_cleaner'
4
+ require 'mongoid'
5
+ require 'mongoid/ids'
6
+ require 'benchmark'
7
+
8
+ Mongoid.configure do |config|
9
+ config.connect_to("mongoid_ids_benchmark2")
10
+ end
11
+
12
+ DatabaseCleaner.strategy = :truncation
13
+
14
+ # start benchmarks
15
+
16
+ TOKEN_LENGTH = 5
17
+
18
+ class Link
19
+ include Mongoid::Document
20
+ include Mongoid::Ids
21
+ field :url
22
+ token :length => TOKEN_LENGTH, :contains => :alphanumeric
23
+ end
24
+
25
+ class NoIdsLink
26
+ include Mongoid::Document
27
+ field :url
28
+ end
29
+
30
+ def create_link(token = true)
31
+ if token
32
+ Link.create(:url => "http://involved.com.au")
33
+ else
34
+ NoIdsLink.create(:url => "http://involved.com.au")
35
+ end
36
+ end
37
+
38
+ Link.delete_all
39
+ Link.create_indexes
40
+ NoIdsLink.delete_all
41
+ num_records = [1, 50, 100, 1000, 2000, 3000, 5000, 10000, 30000, 50000]
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
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ require 'mongoid/ids/collisions'
2
+
3
+ module Mongoid
4
+ module Ids
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::Ids::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 Ids
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
+ 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::Ids] Warning: Maximum token generation retries (#{retry_count}) exceeded on `#{field_name}'." if defined?(Rails)
23
+ raise Mongoid::Ids::CollisionRetriesExceeded.new(self, retry_count)
24
+ end
25
+
26
+ def is_duplicate_token_error?(err, document, field_name)
27
+ [11000, 11001].include?(err.details['code']) &&
28
+ err.details['err'] =~ /dup key/ &&
29
+ err.details['err'] =~ /"#{document.send(field_name)}"/
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ module Mongoid
2
+ module Ids
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 Ids
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| BSON::ObjectId.legal?(arg)} ? send(:"find_without_#{field_name}",*args) : klass.send(:"find_by_#{field_name.to_s}", args.first)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,76 @@
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 Ids
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.rand_string_from_chars(chars, length = 1)
44
+ Array.new(length).map{ chars.sample }.join
45
+ end
46
+
47
+ def self.down_character(length = 1)
48
+ self.rand_string_from_chars ('a'..'z').to_a, length
49
+ end
50
+
51
+ def self.up_character(length = 1)
52
+ self.rand_string_from_chars ('A'..'Z').to_a, length
53
+ end
54
+
55
+ def self.integer(length = 1)
56
+ (rand(10**length - 10**(length-1)) + 10**(length-1)).to_s
57
+ end
58
+
59
+ def self.digits(length = 1)
60
+ rand(10**length).to_s.rjust(length, "0")
61
+ end
62
+
63
+ def self.alpha(length = 1)
64
+ self.rand_string_from_chars (('A'..'Z').to_a + ('a'..'z').to_a), length
65
+ end
66
+
67
+ def self.alphanumeric(length = 1)
68
+ (1..length).collect { (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr }.join
69
+ end
70
+
71
+ def self.punctuation(length = 1)
72
+ self.rand_string_from_chars ['.','-','_','=','+','$'], length
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,78 @@
1
+ module Mongoid
2
+ module Ids
3
+ class Options
4
+ def initialize(options = {})
5
+ @options = merge_defaults validate_options(options)
6
+ end
7
+
8
+ def length
9
+ @options[:length]
10
+ end
11
+
12
+ def retry_count
13
+ @options[:retry_count]
14
+ end
15
+
16
+ def contains
17
+ @options[:contains]
18
+ end
19
+
20
+ def field_name
21
+ @options[:field_name]
22
+ end
23
+
24
+ def field_name=(param)
25
+ return if param.nil? || param.empty?
26
+ @options[:field_name] = param
27
+ end
28
+
29
+ def skip_finders?
30
+ @options[:skip_finders]
31
+ end
32
+
33
+ def override_to_param?
34
+ @options[:override_to_param]
35
+ end
36
+
37
+ def pattern
38
+ @options[:pattern] ||= \
39
+ case @options[:contains].to_sym
40
+ when :alphanumeric
41
+ "%s#{@options[:length]}"
42
+ when :alpha
43
+ "%w#{@options[:length]}"
44
+ when :alpha_upper
45
+ "%C#{@options[:length]}"
46
+ when :alpha_lower
47
+ "%c#{@options[:length]}"
48
+ when :numeric
49
+ "%d1,#{@options[:length]}"
50
+ when :fixed_numeric
51
+ "%d#{@options[:length]}"
52
+ when :fixed_numeric_no_leading_zeros
53
+ "%D#{@options[:length]}"
54
+ end
55
+ end
56
+
57
+ private
58
+ def validate_options(options)
59
+ if options.has_key?(:retry)
60
+ STDERR.puts "Mongoid::Ids 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
+ :length => 4,
69
+ :retry_count => 3,
70
+ :contains => :alphanumeric,
71
+ :field_name => :_id,
72
+ :skip_finders => false,
73
+ :override_to_param => true,
74
+ }.merge(options)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module Ids
3
+ VERSION = '0.1.1'
4
+ end
5
+ end