token_attr 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +6 -6
- data/lib/token_attr.rb +2 -87
- data/lib/token_attr/concern.rb +84 -0
- data/lib/token_attr/errors.rb +14 -0
- data/lib/token_attr/version.rb +1 -1
- data/spec/spec_helper.rb +6 -0
- data/spec/{token_attr_spec.rb → token_attr/concern_spec.rb} +60 -20
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8883c5c7026e366c148643bd1e4bd13dc57feba4
|
4
|
+
data.tar.gz: 7f661a16d09ab1e9368680efff5b940e20c51f05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a01317959dd57120d37231389020af8e9ea669596788aad13b50c60b1f170f3cf3155ba20e640bbdd2e1076657bb08e8c218b592c4c19c33f98fada74accb73f
|
7
|
+
data.tar.gz: 404e200b4bfe0c10ddeee8cf1b9970cc39c0ee572815e7c3c5ba11c3737ffc47f9f82521faefb043120d21dbbcf4ebdd4ac415cbd1c8e56a4e57023317c4a970
|
data/README.md
CHANGED
@@ -6,13 +6,13 @@ Unique random token generator for ActiveRecord.
|
|
6
6
|
|
7
7
|
Add `token_attr` to your Gemfile:
|
8
8
|
|
9
|
-
gem 'token_attr', '~> 0.
|
9
|
+
gem 'token_attr', '~> 0.2.0'
|
10
10
|
|
11
11
|
## Usage
|
12
12
|
|
13
13
|
```
|
14
14
|
class User < ActiveRecord::Base
|
15
|
-
include TokenAttr
|
15
|
+
include TokenAttr::Concern
|
16
16
|
token_attr :token
|
17
17
|
end
|
18
18
|
|
@@ -21,7 +21,7 @@ user.valid?
|
|
21
21
|
user.token # => "b8bd30ff"
|
22
22
|
```
|
23
23
|
|
24
|
-
The token is generated in a `before_validation` callback
|
24
|
+
The token is generated in a `before_validation` callback only if the it's `nil`.
|
25
25
|
|
26
26
|
### Options
|
27
27
|
|
@@ -48,10 +48,10 @@ Accepted values:
|
|
48
48
|
- a string - a string of your choice of the characters you want to use
|
49
49
|
|
50
50
|
```
|
51
|
-
token_attr :token, alphabet: :numeric
|
52
|
-
token_attr :token, alphabet: :alphabetic
|
51
|
+
token_attr :token, alphabet: :numeric # => "82051173"
|
52
|
+
token_attr :token, alphabet: :alphabetic # => "xqnInSJa"
|
53
53
|
token_attr :token, alphabet: :alphanumeric # => "61nD0lUo"
|
54
|
-
token_attr :token, alphabet: "token"
|
54
|
+
token_attr :token, alphabet: "token" # => "ktnekoet"
|
55
55
|
```
|
56
56
|
|
57
57
|
## Contributing
|
data/lib/token_attr.rb
CHANGED
@@ -1,88 +1,3 @@
|
|
1
1
|
require 'token_attr/version'
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
|
5
|
-
module TokenAttr
|
6
|
-
extend ActiveSupport::Concern
|
7
|
-
|
8
|
-
DEFAULT_TOKEN_LENGTH = 8.freeze
|
9
|
-
ALPHABETIC_ALPHABET = [('a'..'z'),('A'..'Z')].map(&:to_a).flatten.freeze
|
10
|
-
NUMERIC_ALPHABET = [(0..9)].map(&:to_a).flatten.freeze
|
11
|
-
ALPHANUMERIC_ALPHABET = [ALPHABETIC_ALPHABET, NUMERIC_ALPHABET].flatten.freeze
|
12
|
-
|
13
|
-
class TooManyAttemptsError < StandardError
|
14
|
-
attr_reader :attribute, :token
|
15
|
-
|
16
|
-
def initialize(attr_name, token, message = nil)
|
17
|
-
@attribute = attr_name
|
18
|
-
@token = token
|
19
|
-
message ||= "Can't generate unique token for \"#{attr_name}\". Last attempt with \"#{token}\"."
|
20
|
-
super(message)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
included do
|
25
|
-
before_validation :generate_tokens
|
26
|
-
end
|
27
|
-
|
28
|
-
module ClassMethods
|
29
|
-
def token_attr(attr_name, options = {})
|
30
|
-
token_attributes << attr_name
|
31
|
-
|
32
|
-
define_method "should_generate_new_#{attr_name}_token?" do
|
33
|
-
send(attr_name).blank?
|
34
|
-
end
|
35
|
-
|
36
|
-
define_method "generate_new_#{attr_name}_token" do
|
37
|
-
token_length = options.fetch(:length, DEFAULT_TOKEN_LENGTH)
|
38
|
-
|
39
|
-
if alphabet = options[:alphabet]
|
40
|
-
alphabet_array = case alphabet
|
41
|
-
when :alphanumeric
|
42
|
-
ALPHANUMERIC_ALPHABET
|
43
|
-
when :alphabetic
|
44
|
-
ALPHABETIC_ALPHABET
|
45
|
-
when :numeric
|
46
|
-
NUMERIC_ALPHABET
|
47
|
-
else
|
48
|
-
alphabet.split('')
|
49
|
-
end
|
50
|
-
(0...token_length).map{ alphabet_array.sample }.join
|
51
|
-
else
|
52
|
-
hex_length = (token_length / 2.0).ceil # 2 characters per length
|
53
|
-
SecureRandom.hex(hex_length).slice(0...token_length)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def token_attributes
|
59
|
-
@token_attributes ||= []
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def generate_tokens
|
64
|
-
self.class.token_attributes.each do |attr_name|
|
65
|
-
if send("should_generate_new_#{attr_name}_token?")
|
66
|
-
new_token = nil
|
67
|
-
try_count = 0
|
68
|
-
begin
|
69
|
-
raise TooManyAttemptsError.new(attr_name, new_token) if try_count == 5
|
70
|
-
new_token = send("generate_new_#{attr_name}_token")
|
71
|
-
try_count += 1
|
72
|
-
end until token_is_unique?(attr_name, new_token)
|
73
|
-
|
74
|
-
send "#{attr_name}=", new_token
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def token_is_unique?(attr_name, token)
|
80
|
-
scope = self.class.where(attr_name => token)
|
81
|
-
scope = scope.where(id != self.id) if self.persisted?
|
82
|
-
!scope.exists?
|
83
|
-
end
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
# Uncomment to auto-extend ActiveRecord, probably not a good idea
|
88
|
-
# ActiveRecord::Base.send(:extend, TokenAttr)
|
2
|
+
require 'token_attr/concern'
|
3
|
+
require 'token_attr/errors'
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/concern'
|
3
|
+
|
4
|
+
module TokenAttr
|
5
|
+
DEFAULT_TOKEN_LENGTH = 8.freeze
|
6
|
+
ALPHABETIC_ALPHABET = [('a'..'z'),('A'..'Z')].map(&:to_a).flatten.freeze
|
7
|
+
NUMERIC_ALPHABET = [(0..9)].map(&:to_a).flatten.freeze
|
8
|
+
ALPHANUMERIC_ALPHABET = [ALPHABETIC_ALPHABET, NUMERIC_ALPHABET].flatten.freeze
|
9
|
+
|
10
|
+
TokenDefinition = Struct.new(:attr_name, :scope_attr)
|
11
|
+
|
12
|
+
module Concern
|
13
|
+
extend ActiveSupport::Concern
|
14
|
+
|
15
|
+
included do
|
16
|
+
before_validation :generate_tokens
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def token_attr(attr_name, options = {})
|
21
|
+
token_definitions << TokenDefinition.new(attr_name, options[:scope])
|
22
|
+
|
23
|
+
define_method "should_generate_new_#{attr_name}?" do
|
24
|
+
send(attr_name).blank?
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method "generate_new_#{attr_name}" do
|
28
|
+
token_length = options.fetch(:length, DEFAULT_TOKEN_LENGTH)
|
29
|
+
|
30
|
+
if alphabet = options[:alphabet]
|
31
|
+
alphabet_array = case alphabet
|
32
|
+
when :alphanumeric
|
33
|
+
ALPHANUMERIC_ALPHABET
|
34
|
+
when :alphabetic
|
35
|
+
ALPHABETIC_ALPHABET
|
36
|
+
when :numeric
|
37
|
+
NUMERIC_ALPHABET
|
38
|
+
else
|
39
|
+
alphabet.split('')
|
40
|
+
end
|
41
|
+
(0...token_length).map{ alphabet_array.sample }.join
|
42
|
+
else
|
43
|
+
hex_length = (token_length / 2.0).ceil # 2 characters per length
|
44
|
+
SecureRandom.hex(hex_length).slice(0...token_length)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def token_definitions
|
50
|
+
@token_definitions ||= []
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate_tokens
|
55
|
+
self.class.token_definitions.each do |td|
|
56
|
+
if send("should_generate_new_#{td.attr_name}?")
|
57
|
+
new_token = nil
|
58
|
+
try_count = 0
|
59
|
+
begin
|
60
|
+
raise TooManyAttemptsError.new(td.attr_name, new_token) if try_count == 5
|
61
|
+
new_token = send("generate_new_#{td.attr_name}")
|
62
|
+
try_count += 1
|
63
|
+
end until token_is_unique?(td, new_token)
|
64
|
+
|
65
|
+
send "#{td.attr_name}=", new_token
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def token_is_unique?(token_definition, token)
|
71
|
+
attr_name = token_definition.attr_name
|
72
|
+
scope_attr = token_definition.scope_attr
|
73
|
+
|
74
|
+
scope = self.class.where(attr_name => token)
|
75
|
+
scope = scope.where.not(id: self.id) if self.persisted?
|
76
|
+
scope = scope.where(scope_attr => read_attribute(scope_attr)) if scope_attr
|
77
|
+
!scope.exists?
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Uncomment to auto-extend ActiveRecord, probably not a good idea
|
84
|
+
# ActiveRecord::Base.send(:extend, TokenAttr)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module TokenAttr
|
2
|
+
|
3
|
+
class TooManyAttemptsError < StandardError
|
4
|
+
attr_reader :attribute, :token
|
5
|
+
|
6
|
+
def initialize(attr_name, token, message = nil)
|
7
|
+
@attribute = attr_name
|
8
|
+
@token = token
|
9
|
+
message ||= "Can't generate unique token for \"#{attr_name}\". Last attempt with \"#{token}\"."
|
10
|
+
super(message)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
data/lib/token_attr/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -20,6 +20,12 @@ ActiveRecord::Schema.define do
|
|
20
20
|
create_table :models, force: true do |t|
|
21
21
|
t.string :token
|
22
22
|
t.string :private_token
|
23
|
+
t.integer :scope_id
|
23
24
|
end
|
24
25
|
|
25
26
|
end
|
27
|
+
|
28
|
+
class BaseModel < ActiveRecord::Base
|
29
|
+
self.table_name = 'models'
|
30
|
+
include TokenAttr::Concern
|
31
|
+
end
|
@@ -2,48 +2,47 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe TokenAttr do
|
4
4
|
|
5
|
-
class Model <
|
6
|
-
include TokenAttr
|
5
|
+
class Model < BaseModel
|
7
6
|
token_attr :token
|
8
7
|
end
|
9
8
|
|
10
|
-
class ModelWithLength <
|
11
|
-
self.table_name = 'models'
|
12
|
-
include TokenAttr
|
9
|
+
class ModelWithLength < BaseModel
|
13
10
|
token_attr :token, length: 13
|
14
11
|
end
|
15
12
|
|
16
|
-
class ModelWithAlphabet <
|
17
|
-
self.table_name = 'models'
|
18
|
-
include TokenAttr
|
13
|
+
class ModelWithAlphabet < BaseModel
|
19
14
|
token_attr :token, alphabet: 'abc123'
|
20
15
|
end
|
21
16
|
|
22
|
-
class ModelWithAlphanumericAlphabet <
|
23
|
-
self.table_name = 'models'
|
24
|
-
include TokenAttr
|
17
|
+
class ModelWithAlphanumericAlphabet < BaseModel
|
25
18
|
token_attr :token, alphabet: :alphanumeric
|
26
19
|
end
|
27
20
|
|
28
|
-
class ModelWithAlphabeticAlphabet <
|
29
|
-
self.table_name = 'models'
|
30
|
-
include TokenAttr
|
21
|
+
class ModelWithAlphabeticAlphabet < BaseModel
|
31
22
|
token_attr :token, alphabet: :alphabetic
|
32
23
|
end
|
33
24
|
|
34
|
-
class ModelWithNumericAlphabet <
|
35
|
-
self.table_name = 'models'
|
36
|
-
include TokenAttr
|
25
|
+
class ModelWithNumericAlphabet < BaseModel
|
37
26
|
token_attr :token, alphabet: :numeric
|
38
27
|
end
|
39
28
|
|
40
|
-
class ModelWithMultipleTokens <
|
41
|
-
self.table_name = 'models'
|
42
|
-
include TokenAttr
|
29
|
+
class ModelWithMultipleTokens < BaseModel
|
43
30
|
token_attr :token
|
44
31
|
token_attr :private_token
|
45
32
|
end
|
46
33
|
|
34
|
+
class ModelWithOverride < BaseModel
|
35
|
+
token_attr :token
|
36
|
+
|
37
|
+
def should_generate_new_token?
|
38
|
+
token == '1234'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class ModelWithScope < BaseModel
|
43
|
+
token_attr :token, scope: :scope_id
|
44
|
+
end
|
45
|
+
|
47
46
|
describe ".token_attr" do
|
48
47
|
let(:model) { Model.new }
|
49
48
|
|
@@ -136,6 +135,47 @@ describe TokenAttr do
|
|
136
135
|
end
|
137
136
|
end
|
138
137
|
|
138
|
+
context "when the should_generate_new_[attr_name]? method is overridden" do
|
139
|
+
let(:model) { ModelWithOverride.new }
|
140
|
+
|
141
|
+
it "generates a token when the condition is satisfied" do
|
142
|
+
SecureRandom.should_receive(:hex).with(4).and_return('newtoken')
|
143
|
+
model.token = '1234'
|
144
|
+
model.valid?
|
145
|
+
model.token.should == 'newtoken'
|
146
|
+
end
|
147
|
+
|
148
|
+
it "does not generate a new token when the condition is not satisfied" do
|
149
|
+
model.valid?
|
150
|
+
model.token.should be_nil
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "with scope" do
|
155
|
+
let(:model) { ModelWithScope.new }
|
156
|
+
|
157
|
+
context "when the scope attributes are different" do
|
158
|
+
it "allows duplicate tokens" do
|
159
|
+
SecureRandom.should_receive(:hex).twice.with(4).and_return('12345678')
|
160
|
+
ModelWithScope.create(scope_id: 1)
|
161
|
+
model.scope_id = 2
|
162
|
+
model.valid?
|
163
|
+
model.token.should == '12345678'
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context "when the scope attributes are the same" do
|
168
|
+
it "regenerates a duplicate token" do
|
169
|
+
SecureRandom.should_receive(:hex).twice.with(4).and_return('12345678')
|
170
|
+
SecureRandom.should_receive(:hex).once.with(4).and_return('asdfghjk')
|
171
|
+
ModelWithScope.create(scope_id: 1)
|
172
|
+
model.scope_id = 1
|
173
|
+
model.valid?
|
174
|
+
model.token.should == 'asdfghjk'
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
139
179
|
context "when token is not blank" do
|
140
180
|
before { model.token = 'not blank' }
|
141
181
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: token_attr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michel Billard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-05-
|
11
|
+
date: 2014-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -109,9 +109,11 @@ files:
|
|
109
109
|
- README.md
|
110
110
|
- Rakefile
|
111
111
|
- lib/token_attr.rb
|
112
|
+
- lib/token_attr/concern.rb
|
113
|
+
- lib/token_attr/errors.rb
|
112
114
|
- lib/token_attr/version.rb
|
113
115
|
- spec/spec_helper.rb
|
114
|
-
- spec/
|
116
|
+
- spec/token_attr/concern_spec.rb
|
115
117
|
- token_attr.gemspec
|
116
118
|
homepage: http://github.com/mbillard/token_attr
|
117
119
|
licenses:
|
@@ -139,4 +141,4 @@ specification_version: 4
|
|
139
141
|
summary: Unique random token generator for ActiveRecord
|
140
142
|
test_files:
|
141
143
|
- spec/spec_helper.rb
|
142
|
-
- spec/
|
144
|
+
- spec/token_attr/concern_spec.rb
|