acts_as_having_string_id 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 203f83e904e3610801246812ab5c46d198d050c7
4
- data.tar.gz: def4ce48a2648ad8badf8fc8599f71a4495e99e9
3
+ metadata.gz: 4049e75011844a99033b1fd85084942748925201
4
+ data.tar.gz: 68060d24a7ad487dcd45291835d69b143f06537e
5
5
  SHA512:
6
- metadata.gz: 99a188bf7ee46b217d20293c0e0faf5208c24c05f50a8f77a28fe478da77cb2bafb1d583724eb1c8f172e38c34d5f134ad8fe9955f48c6027aeafa9ec32ceb12
7
- data.tar.gz: f768f3a63fd24fd5fdee863ac4e2e3319d6f2eace227cb4c1d88b9f81cf84285b5c40018eedf54eab1b22067575dca4c41375831a51b81f78cddb01617f03153
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 string representation is now available as `id_string` on your model object. As an example:
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.id_string
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 the `id_string` method. For example, if you're using [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers):
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.id_string
96
+ object.id.to_s
78
97
  end
79
98
  end
80
99
 
81
- You can also get the string representation of an id without having the instance
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 id from the string representation
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 < ActiveRecord::Type::Value
3
- def initialize(tea)
4
- @tea = tea
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 serialize(value)
8
- if value.is_a? String
9
- i = @tea.decrypt(value.base62_decode)
10
- if i >= 2**31
11
- # Since Postgres SERIAL is a signed 32-bit integer, we can
12
- # only represent integers up until (2**32)-1. If we're
13
- # serializing a larger id, we want a not found rather than
14
- # a postgres datatype out of bounds error. WHERE id = -1
15
- # will definitely not be found.
16
- return -1
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
@@ -1,3 +1,3 @@
1
1
  module ActsAsHavingStringId
2
- VERSION = '0.1.2'
2
+ VERSION = '0.2.0'
3
3
  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
- attribute :id, ActsAsHavingStringId::StringId.new(_tea)
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.1.2
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-05 00:00:00.000000000 Z
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