acts_as_having_string_id 0.1.2 → 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 +31 -14
- data/lib/acts_as_having_string_id/railtie.rb +2 -2
- data/lib/acts_as_having_string_id/string_id.rb +66 -16
- data/lib/acts_as_having_string_id/version.rb +1 -1
- data/lib/acts_as_having_string_id.rb +9 -9
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4049e75011844a99033b1fd85084942748925201
|
4
|
+
data.tar.gz: 68060d24a7ad487dcd45291835d69b143f06537e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3372a5c525692612ba7ca80000ec6b0c515a3f413ce2324281e722e295aa3627845d8bc6e6638ab5b1e4bc798885abcf65b0989d32a8bce302eef5155dbd1f6
|
7
|
+
data.tar.gz: 053b131ac064ae39f64c9a0ae9fbbd794cde0a1b8e0a2d612f2f0b42b2df85b0642afee540843b2aa8e67c2436037b3b002680884814fd79da9f78496c619799
|
data/README.md
CHANGED
@@ -17,12 +17,25 @@ After
|
|
17
17
|
"name": "Alice O'User"
|
18
18
|
}
|
19
19
|
|
20
|
+
## Problem
|
20
21
|
Exposing sequential integer IDs has several drawbacks:
|
21
22
|
|
22
23
|
* Javascript has a 53-bit limit for integers (see https://dev.twitter.com/overview/api/twitter-ids-json-and-snowflake), which is a problem if you have large IDs
|
23
24
|
* Perhaps you don't want objects to be easily enumerable, even if they're public (if you know about http://example.com/documents/104, it's way too easy to find document 105)
|
24
25
|
* Sequential IDs make it easy to know how much usage your product gets (if my newly created user is http://example.com/users/1337, your product probably has 1,337 users)
|
25
26
|
|
27
|
+
## Why not use UUIDs?
|
28
|
+
"But why not just use UUIDs", you ask? Rails has built-in support for them. But they are very long. Exposing them in an API is okay, but in a URL just doesn't look nice
|
29
|
+
|
30
|
+
http://example.com/objects/be398f64-320f-4731-be73-74699e6795bc
|
31
|
+
|
32
|
+
Even base62 encoding that ID is very long
|
33
|
+
|
34
|
+
http://example.com/objects/27WzQMxpvINgio2w5Xt0hk
|
35
|
+
|
36
|
+
64-bit integers would be optimal, but they can't be random: the risk of collisions would be too high.
|
37
|
+
|
38
|
+
## Our solution
|
26
39
|
Rails makes heavy use of sequential integer IDs internally, but there's no need of exposing them. `ActsAsHavingStringId` provides an alternative string representation of your IDs. This representation is
|
27
40
|
|
28
41
|
base62(tea(id, md5(ModelClass.name + Rails.application.secrets.string_id_key)))
|
@@ -33,8 +46,6 @@ The representation looks something like "E0znqip4mRA".
|
|
33
46
|
|
34
47
|
Your controllers will continue to work without modification, but will start to accept string IDs. So if http://example.com/orders/104 worked before, something like http://example.com/orders/E0znqip4mRA should magically work.
|
35
48
|
|
36
|
-
You do however need to take care never to expose the `id` member of your models. Instead, use `id_string`.
|
37
|
-
|
38
49
|
## Usage
|
39
50
|
First, set up your `secrets.yml`:
|
40
51
|
|
@@ -47,43 +58,51 @@ First, set up your `secrets.yml`:
|
|
47
58
|
production:
|
48
59
|
string_id_key: <%= ENV["STRING_ID_KEY"] %>
|
49
60
|
|
50
|
-
Then, call the method in your model class:
|
61
|
+
Then, call the method in your model class, after any relations to other models:
|
51
62
|
|
52
63
|
class MyModel < ApplicationRecord
|
64
|
+
has_many :my_other_model
|
53
65
|
acts_as_having_string_id
|
54
66
|
end
|
55
67
|
|
56
|
-
The
|
68
|
+
The id of your model will now not be an int, but rather an instance of `ActsAsHavingStringId::StringId`. As an example:
|
57
69
|
|
58
70
|
> m = MyModel.create!
|
59
71
|
> m.id
|
72
|
+
=> 1/7EajpSfdWIf
|
73
|
+
> m.id.to_i
|
60
74
|
=> 1
|
61
|
-
> m.
|
75
|
+
> m.id.to_s
|
62
76
|
=> "7EajpSfdWIf"
|
63
77
|
|
64
|
-
All ActiveRecord functions will also accept the string representation as input:
|
78
|
+
All ActiveRecord functions will continue to accept int IDs, but will now also accept the string representation as input:
|
65
79
|
|
66
80
|
> MyModel.find("7EajpSfdWIf")
|
67
|
-
=> #<MyModel id: 1, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">
|
81
|
+
=> #<MyModel id: 1/7EajpSfdWIf, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">
|
68
82
|
> MyModel.where(id: "7EajpSfdWIf")
|
69
|
-
=> #<ActiveRecord::Relation [#<MyModel id: 1, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">]>
|
83
|
+
=> #<ActiveRecord::Relation [#<MyModel id: 1/7EajpSfdWIf, created_at: "2016-08-31 13:27:02", updated_at: "2016-08-31 13:27:02">]>
|
84
|
+
|
85
|
+
In all associated models, foreign keys to your model will also be this new type of id.
|
86
|
+
|
87
|
+
> MyOtherModel.create! my_model: MyModel.first
|
88
|
+
=> #<MyOtherModel id: 1, my_model_id: 1/GBpjdLndSR0, created_at: "2016-09-07 10:32:24", updated_at: "2016-09-07 10:32:24">
|
70
89
|
|
71
|
-
Then, for exposing your string ID, use
|
90
|
+
Then, for exposing your string ID, make sure to always use `id.to_s`. For example, if you're using [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers):
|
72
91
|
|
73
92
|
class UserSerializer < ActiveModel::Serializer
|
74
93
|
attributes :id, :name
|
75
94
|
|
76
95
|
def id
|
77
|
-
object.
|
96
|
+
object.id.to_s
|
78
97
|
end
|
79
98
|
end
|
80
99
|
|
81
|
-
You can
|
100
|
+
You can get the string representation of an ID from a class without having the instance
|
82
101
|
|
83
102
|
> MyModel.id_string(1)
|
84
103
|
=> "7EajpSfdWIf"
|
85
104
|
|
86
|
-
And, conversely, getting the
|
105
|
+
And, conversely, getting the ID from the string representation
|
87
106
|
|
88
107
|
> MyModel.id_int("7EajpSfdWIf")
|
89
108
|
=> 1
|
@@ -91,10 +110,8 @@ And, conversely, getting the id from the string representation
|
|
91
110
|
And that's just about it!
|
92
111
|
|
93
112
|
## TODO
|
94
|
-
* You should be able to do `MyOtherModel.create! my_model_id: "KuUnDvpJYS2"` and `my_other_model.my_model_id = "KuUnDvpJYS2"`
|
95
113
|
* Since the `MyModel.find("7EajpSfdWIf")` functionality depends on the argument now being a string, `MyModel.find("5")` will no longer mean `MyModel.find(5)`, but rather `MyModel.find(4387534)` or something. Is that a problem?
|
96
114
|
* It's a potential security problem that we don't force strings from controllers (integer id coming from JSON postdata will make it find by original id)
|
97
|
-
* Although TEA handles (and outputs) 64-bit ids, we currently limit the input to 32-bit
|
98
115
|
|
99
116
|
## Installation
|
100
117
|
Add this line to your application's Gemfile:
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module ActsAsHavingStringId
|
2
2
|
class Railtie < Rails::Railtie
|
3
3
|
initializer "railtie.include_in_application_record" do
|
4
|
+
ApplicationRecord.include(ActsAsHavingStringId)
|
5
|
+
|
4
6
|
if defined?(Spring)
|
5
7
|
Spring.after_fork do
|
6
8
|
# This needs to happen every time Spring reloads
|
7
9
|
ApplicationRecord.include(ActsAsHavingStringId)
|
8
10
|
end
|
9
|
-
else
|
10
|
-
ApplicationRecord.include(ActsAsHavingStringId)
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
@@ -1,24 +1,74 @@
|
|
1
1
|
module ActsAsHavingStringId
|
2
|
-
class StringId
|
3
|
-
|
4
|
-
|
2
|
+
class StringId
|
3
|
+
attr_reader :string_value, :int_value
|
4
|
+
|
5
|
+
def initialize(klass, value)
|
6
|
+
if value == nil
|
7
|
+
@string_value = nil
|
8
|
+
@int_value = nil
|
9
|
+
elsif value.is_a? String
|
10
|
+
@string_value = value
|
11
|
+
@int_value = klass.id_int(value)
|
12
|
+
else
|
13
|
+
@int_value = value
|
14
|
+
@string_value = klass.id_string(value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
"#{int_value}/#{string_value}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
string_value
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_i
|
27
|
+
int_value
|
5
28
|
end
|
6
29
|
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
30
|
+
def ==(other)
|
31
|
+
other.is_a?(StringId) && other.int_value == int_value
|
32
|
+
end
|
33
|
+
|
34
|
+
class Type < ActiveRecord::Type::Value
|
35
|
+
def initialize(klass)
|
36
|
+
@klass = klass
|
37
|
+
end
|
38
|
+
|
39
|
+
def type
|
40
|
+
:integer
|
41
|
+
end
|
42
|
+
|
43
|
+
def cast(value)
|
44
|
+
if value == nil
|
45
|
+
nil
|
46
|
+
else
|
47
|
+
ActsAsHavingStringId::StringId(@klass, value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def deserialize(value)
|
52
|
+
if value.is_a?(String) || value.is_a?(Fixnum)
|
53
|
+
ActsAsHavingStringId::StringId(@klass, value)
|
54
|
+
elsif value == nil
|
55
|
+
nil
|
56
|
+
else
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def serialize(value)
|
62
|
+
if value == nil
|
63
|
+
nil
|
64
|
+
else
|
65
|
+
ActsAsHavingStringId::StringId(@klass, value).int_value
|
17
66
|
end
|
18
|
-
return i
|
19
|
-
else
|
20
|
-
value
|
21
67
|
end
|
22
68
|
end
|
23
69
|
end
|
70
|
+
|
71
|
+
def self.StringId(klass, value)
|
72
|
+
value.is_a?(StringId) ? value : StringId.new(klass, value)
|
73
|
+
end
|
24
74
|
end
|
@@ -9,7 +9,15 @@ module ActsAsHavingStringId
|
|
9
9
|
module ClassMethods
|
10
10
|
def acts_as_having_string_id(options = {})
|
11
11
|
class_eval do
|
12
|
-
|
12
|
+
attrib_type = ActsAsHavingStringId::StringId::Type.new(self)
|
13
|
+
attribute :id, attrib_type
|
14
|
+
|
15
|
+
self.reflections.each_value do |r|
|
16
|
+
# Attribute all foreign keys pointing here as well
|
17
|
+
r.klass.class_eval do
|
18
|
+
attribute r.foreign_key.to_sym, attrib_type
|
19
|
+
end
|
20
|
+
end
|
13
21
|
|
14
22
|
def self.id_string(id)
|
15
23
|
# Return the string representation of id
|
@@ -21,8 +29,6 @@ module ActsAsHavingStringId
|
|
21
29
|
_tea.decrypt(id_string.base62_decode)
|
22
30
|
end
|
23
31
|
end
|
24
|
-
|
25
|
-
include ActsAsHavingStringId::LocalInstanceMethods
|
26
32
|
end
|
27
33
|
|
28
34
|
def _tea
|
@@ -30,10 +36,4 @@ module ActsAsHavingStringId
|
|
30
36
|
@_tea ||= ActsAsHavingStringId::TEA.new(pass_phrase)
|
31
37
|
end
|
32
38
|
end
|
33
|
-
|
34
|
-
module LocalInstanceMethods
|
35
|
-
def id_string
|
36
|
-
self.class.id_string(id)
|
37
|
-
end
|
38
|
-
end
|
39
39
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_having_string_id
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Magnus Hult
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -58,6 +58,20 @@ dependencies:
|
|
58
58
|
- - ">="
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: minitest
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
61
75
|
description: Makes a model accept and expose a seemingly random string id
|
62
76
|
email:
|
63
77
|
- magnus@magnushult.se
|