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 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