ksuid 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +12 -3
- data/CONTRIBUTING.md +4 -5
- data/README.md +120 -3
- data/ksuid.gemspec +1 -1
- data/lib/ksuid.rb +71 -2
- data/lib/ksuid/activerecord.rb +75 -0
- data/lib/ksuid/activerecord/binary_type.rb +66 -0
- data/lib/ksuid/activerecord/table_definition.rb +56 -0
- data/lib/ksuid/activerecord/type.rb +65 -0
- data/lib/ksuid/base62.rb +14 -1
- data/lib/ksuid/configuration.rb +94 -0
- data/lib/ksuid/railtie.rb +15 -0
- data/lib/ksuid/type.rb +15 -6
- data/lib/ksuid/utils.rb +3 -4
- data/lib/ksuid/version.rb +1 -1
- metadata +14 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 39789ed89a8ef5f2a949dc5cf716ef87f98904902dd8262e123b325408e6e69c
|
4
|
+
data.tar.gz: e0d3f66c83acab156cc260d063c0a69921b880a83dce3ca32edd0635ed230579
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4a500c7863252cbe4a55a898ee941e800757676ae3a78c5f191a61173267768ea5b8352609e3689295eb233e14551fea87333fe3f0158208154aed17d9035b4
|
7
|
+
data.tar.gz: de9846561e6d4a2282bbbdf16ea524f6160198916e4cee8292968cb58d378cd66b2ea4699a0698a3f95db69070d3307118bb000726cb6ce05192eac20ed73a90
|
data/CHANGELOG.md
CHANGED
@@ -4,12 +4,21 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
-
## [0.1.0
|
7
|
+
## [0.2.0](https://github.com/michaelherold/ksuid/compare/v0.1.0...v0.2.0) - 2020-11-11
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- The ability to configure the random generator for the gem via `KSUID.configure`. This allows you to set up random generation to the specifications you need, whether that is for speed or for security.
|
12
|
+
- Support for ActiveRecord. You can now use `KSUID::ActiveRecord[:my_field]` to define a KSUID field using the Rails 5 Attributes API. There is also two new column types for migrations: `ksuid` and `ksuid_binary`. The first stores your KSUID as a string in the database, the latter as binary data.
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
|
16
|
+
- The `KSUID::Type#inspect` method now makes it much easier to see what you're looking at in the console when you're debugging.
|
17
|
+
|
18
|
+
## [0.1.0](https://github.com/michaelherold/ksuid/tree/v0.1.0) - 2017-11-05
|
8
19
|
|
9
20
|
### Added
|
10
21
|
|
11
22
|
- Basic `KSUID.new` interface.
|
12
23
|
- Parsing of bytes through `KSUID.from_bytes`.
|
13
24
|
- Parsing of strings through `KSUID.from_base62`.
|
14
|
-
|
15
|
-
[0.1.0]: https://github.com/michaelherold/interactor-contracts/tree/v0.1.0
|
data/CONTRIBUTING.md
CHANGED
@@ -28,14 +28,13 @@ Ideally, a bug report should include a pull request with failing specs.
|
|
28
28
|
1. [Fork the repository].
|
29
29
|
2. [Create a topic branch].
|
30
30
|
3. Add specs for your unimplemented feature or bug fix.
|
31
|
-
4. Run `
|
31
|
+
4. Run `appraisal rake spec`. If your specs pass, return to step 3.
|
32
32
|
5. Implement your feature or bug fix.
|
33
|
-
6. Run `
|
33
|
+
6. Run `appraisal rake`. If your specs or any of the linters fail, return to step 5.
|
34
34
|
7. Open `coverage/index.html`. If your changes are not completely covered by your tests, return to step 3.
|
35
35
|
8. Add documentation for your feature or bug fix.
|
36
|
-
9.
|
37
|
-
10.
|
38
|
-
11. [Submit a pull request].
|
36
|
+
9. Commit and push your changes.
|
37
|
+
10. [Submit a pull request].
|
39
38
|
|
40
39
|
[Create a topic branch]: https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/
|
41
40
|
[Fork the repository]: http://learn.github.com/p/branching.html
|
data/README.md
CHANGED
@@ -92,6 +92,123 @@ If you need to generate a KSUID for a specific timestamp, use:
|
|
92
92
|
ksuid = KSUID.new(time: time) # where time is a Time-like object
|
93
93
|
```
|
94
94
|
|
95
|
+
If you need to use a faster or more secure way of generating the random payloads (or if you want the payload to be non-random data), you can configure the gem for those use cases:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
KSUID.configure do |config|
|
99
|
+
config.random_generator = -> { Random.new.bytes(16) }
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
### ActiveRecord
|
104
|
+
|
105
|
+
Whether you are using ActiveRecord inside an existing project or in a new project, usage is simple. Additionally, you can use it with or without Rails.
|
106
|
+
|
107
|
+
#### Adding to an existing model
|
108
|
+
|
109
|
+
Within a Rails project, it is very easy to get started using KSUIDs within your models. You can use the `ksuid` column type in a Rails migration to add a column to an existing model:
|
110
|
+
|
111
|
+
rails generate migration add_ksuid_to_events ksuid:ksuid
|
112
|
+
|
113
|
+
This will generate a migration like the following:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class AddKsuidToEvents < ActiveRecord::Migration[5.2]
|
117
|
+
def change
|
118
|
+
add_column :events, :unique_id, :ksuid
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
Then, to add proper handling to the field, you will want to mix a module into the model:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
class Event < ApplicationRecord
|
127
|
+
include KSUID::ActiveRecord[:unique_id]
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
#### Creating a new model
|
132
|
+
|
133
|
+
To create a new model with a `ksuid` field that is stored as a KSUID, use the `ksuid` column type. Using the Rails generators, this looks like:
|
134
|
+
|
135
|
+
rails generate model Event my_field_name:ksuid
|
136
|
+
|
137
|
+
If you would like to add a KSUID to an existing model, you can do so with the following:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class AddKsuidToEvents < ActiveRecord::Migration[5.2]
|
141
|
+
change_table :events do |table|
|
142
|
+
table.ksuid :my_field_name
|
143
|
+
end
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
Once you have generated the table that you will use for your model, you will need to include a module into the model class, as follows:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class Event < ApplicationRecord
|
151
|
+
include KSUID::ActiveRecord[:my_field_name]
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
##### With a KSUID primary key
|
156
|
+
|
157
|
+
You can also use a KSUID as the primary key on a table, much like you can use a UUID in vanilla Rails. To do so requires a little more finagling than you can manage through the generators. When hand-writing the migration, it will look like this:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
class CreateEvents < ActiveRecord::Migration[5.2]
|
161
|
+
create_table :events, id: false do |table|
|
162
|
+
table.ksuid :id, primary_key: true
|
163
|
+
end
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
You will need to mix in the module into your model as well:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
class Event < ApplicationRecord
|
171
|
+
include KSUID::ActiveRecord[:id]
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
#### Outside of Rails
|
176
|
+
|
177
|
+
Outside of Rails, you cannot rely on the Railtie to load the appropriate files for you automatically. Toward the start of your application's boot process, you will want to require the following:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
require 'ksuid/activerecord'
|
181
|
+
|
182
|
+
# If you will be using the ksuid column type in a migration
|
183
|
+
require 'ksuid/activerecord/table_definition'
|
184
|
+
```
|
185
|
+
|
186
|
+
Once you have required the file(s) that you need, everything else will work as it does above.
|
187
|
+
|
188
|
+
#### Binary vs. String KSUIDs
|
189
|
+
|
190
|
+
These examples all store your identifier as a string-based KSUID. If you would like to use binary KSUIDs instead, use the `ksuid_binary` column type. Unless you need to be super-efficient with your database, we recommend using string-based KSUIDs because it makes looking at the data while in the database a little easier to understand.
|
191
|
+
|
192
|
+
When you include the KSUID module into your model, you will want to pass the `:binary` option as well:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
class Event < ApplicationRecord
|
196
|
+
include KSUID::ActiveRecord[:my_field_name, binary: true]
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
#### Use the KSUID as your `created_at` timestamp
|
201
|
+
|
202
|
+
Since KSUIDs include a timestamp as well, you can infer the `#created_at` timestamp from the KSUID. The module builder enables that option automatically with the `:created_at` option, like so:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
class Event < ApplicationRecord
|
206
|
+
include KSUID::ActiveRecord[:my_field_name, created_at: true]
|
207
|
+
end
|
208
|
+
```
|
209
|
+
|
210
|
+
This allows you to be efficient in your database design if that is a constraint you need to satisfy.
|
211
|
+
|
95
212
|
## Contributing
|
96
213
|
|
97
214
|
So you’re interested in contributing to KSUID? Check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to do that.
|
@@ -100,9 +217,9 @@ So you’re interested in contributing to KSUID? Check out our [contributing gui
|
|
100
217
|
|
101
218
|
This library aims to support and is [tested against][travis] the following Ruby versions:
|
102
219
|
|
103
|
-
* Ruby 2.
|
104
|
-
* Ruby 2.
|
105
|
-
* JRuby 9.
|
220
|
+
* Ruby 2.5
|
221
|
+
* Ruby 2.6
|
222
|
+
* JRuby 9.2
|
106
223
|
|
107
224
|
If something doesn't work on one of these versions, it's a bug.
|
108
225
|
|
data/ksuid.gemspec
CHANGED
data/lib/ksuid.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'ksuid/configuration'
|
3
4
|
require_relative 'ksuid/type'
|
4
5
|
require_relative 'ksuid/version'
|
5
6
|
|
@@ -74,6 +75,57 @@ module KSUID
|
|
74
75
|
# @return [String]
|
75
76
|
MAX_STRING_ENCODED = 'aWgEPTl1tmebfsQzFP4bxwgy80V'
|
76
77
|
|
78
|
+
# Converts a KSUID-compatible value into an actual KSUID
|
79
|
+
#
|
80
|
+
# @api public
|
81
|
+
#
|
82
|
+
# @example Converts a base 62 KSUID string into a KSUID
|
83
|
+
# KSUID.call('15Ew2nYeRDscBipuJicYjl970D1')
|
84
|
+
#
|
85
|
+
# @param ksuid [String, Array<Integer>, KSUID::Type] the KSUID-compatible value
|
86
|
+
# @return [KSUID::Type] the converted KSUID
|
87
|
+
# @raise [ArgumentError] if the value is not KSUID-compatible
|
88
|
+
def self.call(ksuid)
|
89
|
+
return unless ksuid
|
90
|
+
|
91
|
+
case ksuid
|
92
|
+
when KSUID::Type then ksuid
|
93
|
+
when Array then KSUID.from_bytes(ksuid)
|
94
|
+
when String then cast_string(ksuid)
|
95
|
+
else
|
96
|
+
raise ArgumentError, "Cannot convert #{ksuid.inspect} to KSUID"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# The configuration for creating new KSUIDs
|
101
|
+
#
|
102
|
+
# @api private
|
103
|
+
#
|
104
|
+
# @return [KSUID::Configuration] the gem's configuration
|
105
|
+
def self.config
|
106
|
+
@config ||= KSUID::Configuration.new
|
107
|
+
end
|
108
|
+
|
109
|
+
# Configures the KSUID gem by passing a block
|
110
|
+
#
|
111
|
+
# @api public
|
112
|
+
#
|
113
|
+
# @example Override the random generator with a null data generator
|
114
|
+
# KSUID.configure do |config|
|
115
|
+
# config.random_generator = -> { "\x00" * KSUID::BYTES[:payload] }
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# @example Override the random generator with the faster, but less secure, Random
|
119
|
+
# KSUID.configure do |config|
|
120
|
+
# config.random_generator = -> { Random.new.bytes(KSUID::BYTES[:payload]) }
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# @return [KSUID::Configuration] the gem's configuration
|
124
|
+
def self.configure
|
125
|
+
yield config if block_given?
|
126
|
+
config
|
127
|
+
end
|
128
|
+
|
77
129
|
# Converts a base 62-encoded string into a KSUID
|
78
130
|
#
|
79
131
|
# @api public
|
@@ -98,7 +150,7 @@ module KSUID
|
|
98
150
|
# @example Parse a KSUID byte string into an object
|
99
151
|
# KSUID.from_bytes("\x06\x83\xF7\x89\x04\x9C\xC2\x15\xC0\x99\xD4+xM\xBE\x994\e\xD7\x9C")
|
100
152
|
#
|
101
|
-
# @param bytes [String
|
153
|
+
# @param bytes [String, Array<Integer>] the byte string or array to convert into an object
|
102
154
|
# @return [KSUID::Type] the KSUID generated from the bytes
|
103
155
|
def self.from_bytes(bytes)
|
104
156
|
bytes = bytes.bytes if bytes.is_a?(String)
|
@@ -131,10 +183,27 @@ module KSUID
|
|
131
183
|
# @example Generate a new KSUID for a given timestamp
|
132
184
|
# KSUID.new(time: Time.parse('2017-11-05 15:00:04 UTC'))
|
133
185
|
#
|
134
|
-
# @param payload [String
|
186
|
+
# @param payload [String, Array<Integer>, nil] the payload for the KSUID
|
135
187
|
# @param time [Time] the timestamp to use for the KSUID
|
136
188
|
# @return [KSUID::Type] the generated KSUID
|
137
189
|
def self.new(payload: nil, time: Time.now)
|
138
190
|
Type.new(payload: payload, time: time)
|
139
191
|
end
|
192
|
+
|
193
|
+
# Casts a string into a KSUID
|
194
|
+
#
|
195
|
+
# @api private
|
196
|
+
#
|
197
|
+
# @param ksuid [String] the string to convert into a KSUID
|
198
|
+
# @return [KSUID::Type] the converted KSUID
|
199
|
+
def self.cast_string(ksuid)
|
200
|
+
if Base62.compatible?(ksuid)
|
201
|
+
KSUID.from_base62(ksuid)
|
202
|
+
else
|
203
|
+
KSUID.from_bytes(ksuid)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
private_class_method :cast_string
|
140
207
|
end
|
208
|
+
|
209
|
+
require 'ksuid/railtie' if defined?(Rails)
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ksuid/activerecord/binary_type'
|
4
|
+
require 'ksuid/activerecord/type'
|
5
|
+
|
6
|
+
module KSUID
|
7
|
+
# Enables an Active Record model to have a KSUID attribute
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
module ActiveRecord
|
11
|
+
# Builds a module to include into the model
|
12
|
+
#
|
13
|
+
# @api public
|
14
|
+
#
|
15
|
+
# @example Add a `#ksuid` attribute to a model
|
16
|
+
# class Event < ActiveRecord::Base
|
17
|
+
# include KSUID::ActiveRecord[:ksuid]
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example Add a `#remote_id` attribute to a model and overrides `#created_at` to use the KSUID
|
21
|
+
# class Event < ActiveRecord::Base
|
22
|
+
# include KSUID::ActiveRecord[:remote_id, created_at: true]
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @param field [String, Symbol] the name of the field to use as a KSUID
|
26
|
+
# @param created_at [Boolean] whether to override the `#created_at` method
|
27
|
+
# @param binary [Boolean] whether to store the KSUID as a binary or a string
|
28
|
+
# @return [Module] the module to include into the model
|
29
|
+
def self.[](field, created_at: false, binary: false)
|
30
|
+
Module
|
31
|
+
.new
|
32
|
+
.tap do |mod|
|
33
|
+
define_attribute(field, mod, binary)
|
34
|
+
define_created_at(field, mod) if created_at
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Defines the attribute method that will be written in the module
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
#
|
42
|
+
# @param field [String, Symbol] the name of the field to set as an attribute
|
43
|
+
# @param mod [Module] the module to extend
|
44
|
+
# @return [void]
|
45
|
+
def self.define_attribute(field, mod, binary)
|
46
|
+
type = 'ksuid'
|
47
|
+
type = 'ksuid_binary' if binary
|
48
|
+
|
49
|
+
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
50
|
+
def self.included(base)
|
51
|
+
base.__send__(:attribute, :#{field}, :#{type}, default: -> { KSUID.new })
|
52
|
+
end
|
53
|
+
RUBY
|
54
|
+
end
|
55
|
+
private_class_method :define_attribute
|
56
|
+
|
57
|
+
# Defines the `#created_at` method that will be written in the module
|
58
|
+
#
|
59
|
+
# @api private
|
60
|
+
#
|
61
|
+
# @param field [String, Symbol] the name of the KSUID attribute field
|
62
|
+
# @param mod [Module] the module to extend
|
63
|
+
# @return [void]
|
64
|
+
def self.define_created_at(field, mod)
|
65
|
+
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
66
|
+
def created_at
|
67
|
+
return unless #{field}
|
68
|
+
|
69
|
+
#{field}.to_time
|
70
|
+
end
|
71
|
+
RUBY
|
72
|
+
end
|
73
|
+
private_class_method :define_created_at
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KSUID
|
4
|
+
module ActiveRecord
|
5
|
+
# A binary-serialized KSUID for storage within an ActiveRecord database
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
#
|
9
|
+
# @example Set an attribute as a KSUID using the verbose syntax
|
10
|
+
# class Event < ActiveRecord::Base
|
11
|
+
# attribute :ksuid, KSUID::ActiveRecord::BinaryType.new, default: -> { KSUID.new }
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @example Set an attribute as a KSUID using the pre-registered type
|
15
|
+
# class Event < ActiveRecord::Base
|
16
|
+
# attribute :ksuid, :ksuid_binary, default: -> { KSUID.new }
|
17
|
+
# end
|
18
|
+
class BinaryType < ::ActiveRecord::Type::Binary
|
19
|
+
# Casts a value from user input into a KSUID
|
20
|
+
#
|
21
|
+
# Type casting happens via the attribute setter and can take input from
|
22
|
+
# many places, including:
|
23
|
+
#
|
24
|
+
# 1. The Rails form builder
|
25
|
+
# 2. Directly from the attribute setter
|
26
|
+
# 3. From the model initializer
|
27
|
+
#
|
28
|
+
# @param value [String, Array<Integer>, KSUID::Type] the value to cast into a KSUID
|
29
|
+
# @return [KSUID::Type] the type-casted value
|
30
|
+
def cast(value)
|
31
|
+
KSUID.call(value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Converts a value from database input to a KSUID
|
35
|
+
#
|
36
|
+
# @param value [String, nil] the database-serialized KSUID to convert
|
37
|
+
# @return [KSUID::Type] the deserialized KSUID
|
38
|
+
def deserialize(value)
|
39
|
+
return unless value
|
40
|
+
|
41
|
+
value = value.to_s if value.is_a?(::ActiveRecord::Type::Binary::Data)
|
42
|
+
KSUID.call(value)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Casts the value from a KSUID into a database-understandable format
|
46
|
+
#
|
47
|
+
# @param value [KSUID::Type, nil] the KSUID in Ruby format
|
48
|
+
# @return [String, nil] the base 62-encoded KSUID for storage in the database
|
49
|
+
def serialize(value)
|
50
|
+
return unless value
|
51
|
+
|
52
|
+
super(KSUID.call(value).to_bytes)
|
53
|
+
end
|
54
|
+
|
55
|
+
# The identifier to use within ActiveRecord's type registry
|
56
|
+
#
|
57
|
+
# @api private
|
58
|
+
# @return [Symbol]
|
59
|
+
def type
|
60
|
+
:ksuid_binary
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
ActiveRecord::Type.register(:ksuid_binary, KSUID::ActiveRecord::BinaryType)
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KSUID
|
4
|
+
module ActiveRecord
|
5
|
+
# Extends ActiveRecord's table definition language for KSUIDs
|
6
|
+
module TableDefinition
|
7
|
+
# Defines a field as a string-based KSUID
|
8
|
+
#
|
9
|
+
# @example Define a KSUID field as a non-primary key
|
10
|
+
# ActiveRecord::Schema.define do
|
11
|
+
# create_table :events, force: true do |table|
|
12
|
+
# table.ksuid :ksuid, index: true, unique: true
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @example Define a KSUID field as a primary key
|
17
|
+
# ActiveRecord::Schema.define do
|
18
|
+
# create_table :events, force: true, id: false do |table|
|
19
|
+
# table.ksuid :id, primary_key: true
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @param args [Array<Symbol>] the list of fields to define as KSUIDs
|
24
|
+
# @param options [Hash] see {ActiveRecord::ConnectionAdapters::TableDefinition}
|
25
|
+
# @return [void]
|
26
|
+
def ksuid(*args, **options)
|
27
|
+
args.each { |name| column(name, :string, **options.merge(limit: 27)) }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Defines a field as a binary-based KSUID
|
31
|
+
#
|
32
|
+
# @example Define a KSUID field as a non-primary key
|
33
|
+
# ActiveRecord::Schema.define do
|
34
|
+
# create_table :events, force: true do |table|
|
35
|
+
# table.ksuid_binary :ksuid, index: true, unique: true
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @example Define a KSUID field as a primary key
|
40
|
+
# ActiveRecord::Schema.define do
|
41
|
+
# create_table :events, force: true, id: false do |table|
|
42
|
+
# table.ksuid_binary :id, primary_key: true
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# @param args [Array<Symbol>] the list of fields to define as KSUIDs
|
47
|
+
# @param options [Hash] see {ActiveRecord::ConnectionAdapters::TableDefinition}
|
48
|
+
# @return [void]
|
49
|
+
def ksuid_binary(*args, **options)
|
50
|
+
args.each { |name| column(name, :binary, **options.merge(limit: 20)) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
ActiveRecord::ConnectionAdapters::TableDefinition.include(KSUID::ActiveRecord::TableDefinition)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KSUID
|
4
|
+
module ActiveRecord
|
5
|
+
# A string-serialized KSUID for storage within an ActiveRecord database
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
#
|
9
|
+
# @example Set an attribute as a KSUID using the verbose syntax
|
10
|
+
# class Event < ActiveRecord::Base
|
11
|
+
# attribute :ksuid, KSUID::ActiveRecord::Type.new, default: -> { KSUID.new }
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# @example Set an attribute as a KSUID using the pre-registered type
|
15
|
+
# class Event < ActiveRecord::Base
|
16
|
+
# attribute :ksuid, :ksuid, default: -> { KSUID.new }
|
17
|
+
# end
|
18
|
+
class Type < ::ActiveRecord::Type::String
|
19
|
+
# Casts a value from user input into a KSUID
|
20
|
+
#
|
21
|
+
# Type casting happens via the attribute setter and can take input from
|
22
|
+
# many places, including:
|
23
|
+
#
|
24
|
+
# 1. The Rails form builder
|
25
|
+
# 2. Directly from the attribute setter
|
26
|
+
# 3. From the model initializer
|
27
|
+
#
|
28
|
+
# @param value [String, Array<Integer>, KSUID::Type] the value to cast into a KSUID
|
29
|
+
# @return [KSUID::Type] the type-casted value
|
30
|
+
def cast(value)
|
31
|
+
KSUID.call(value)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Converts a value from database input to a KSUID
|
35
|
+
#
|
36
|
+
# @param value [String, nil] the database-serialized KSUID to convert
|
37
|
+
# @return [KSUID::Type] the deserialized KSUID
|
38
|
+
def deserialize(value)
|
39
|
+
return unless value
|
40
|
+
|
41
|
+
KSUID.from_base62(value)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Casts the value from a KSUID into a database-understandable format
|
45
|
+
#
|
46
|
+
# @param value [KSUID::Type, nil] the KSUID in Ruby format
|
47
|
+
# @return [String, nil] the base 62-encoded KSUID for storage in the database
|
48
|
+
def serialize(value)
|
49
|
+
return unless value
|
50
|
+
|
51
|
+
KSUID.call(value).to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
# The identifier to use within ActiveRecord's type registry
|
55
|
+
#
|
56
|
+
# @api private
|
57
|
+
# @return [Symbol]
|
58
|
+
def type
|
59
|
+
:ksuid
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
ActiveRecord::Type.register(:ksuid, KSUID::ActiveRecord::Type)
|
data/lib/ksuid/base62.rb
CHANGED
@@ -20,6 +20,19 @@ module KSUID
|
|
20
20
|
# @api private
|
21
21
|
BASE = CHARSET.size
|
22
22
|
|
23
|
+
# Checks whether a string is a base 62-compatible string
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
#
|
27
|
+
# @example Checks a KSUID for base 62 compatibility
|
28
|
+
# KSUID::Base62.compatible?("15Ew2nYeRDscBipuJicYjl970D1") #=> true
|
29
|
+
#
|
30
|
+
# @param string [String] the string to check for compatibility
|
31
|
+
# @return [Boolean]
|
32
|
+
def self.compatible?(string)
|
33
|
+
string.each_char.all? { |char| CHARSET.include?(char) }
|
34
|
+
end
|
35
|
+
|
23
36
|
# Decodes a base 62-encoded string into an integer
|
24
37
|
#
|
25
38
|
# @api public
|
@@ -71,7 +84,7 @@ module KSUID
|
|
71
84
|
# 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
|
72
85
|
# )
|
73
86
|
#
|
74
|
-
# @param bytes [String
|
87
|
+
# @param bytes [String, Array<Integer>] the bytes to encode
|
75
88
|
# @return [String] the encoded bytes as a base 62 string
|
76
89
|
def self.encode_bytes(bytes)
|
77
90
|
encode(Utils.int_from_bytes(bytes))
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module KSUID
|
6
|
+
# Encapsulates the configuration for the KSUID gem as a whole.
|
7
|
+
#
|
8
|
+
# You can override the generation of the random payload data by setting the
|
9
|
+
# {#random_generator} value to a valid random payload generator. This should
|
10
|
+
# be done via the module-level {KSUID.configure} method.
|
11
|
+
#
|
12
|
+
# The gem-level configuration lives at the module-level {KSUID.config}.
|
13
|
+
#
|
14
|
+
# @api semipublic
|
15
|
+
class Configuration
|
16
|
+
# Raised when the gem is misconfigured.
|
17
|
+
ConfigurationError = Class.new(StandardError)
|
18
|
+
|
19
|
+
# The default generator for generating random payloads
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
#
|
23
|
+
# @return [Proc]
|
24
|
+
def self.default_generator
|
25
|
+
-> { SecureRandom.random_bytes(BYTES[:payload]) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Instantiates a new KSUID configuration
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
#
|
32
|
+
# @return [KSUID::Configuration] the new configuration
|
33
|
+
def initialize
|
34
|
+
self.random_generator = self.class.default_generator
|
35
|
+
end
|
36
|
+
|
37
|
+
# The method for generating random payloads in the gem
|
38
|
+
#
|
39
|
+
# @api private
|
40
|
+
#
|
41
|
+
# @return [#call] a callable that returns 16 bytes
|
42
|
+
attr_reader :random_generator
|
43
|
+
|
44
|
+
# Override the method for generating random payloads in the gem
|
45
|
+
#
|
46
|
+
# @api semipublic
|
47
|
+
#
|
48
|
+
# @example Override the random generator with a null data generator
|
49
|
+
# KSUID.configure do |config|
|
50
|
+
# config.random_generator = -> { "\x00" * KSUID::BYTES[:payload] }
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# @example Override the random generator with the faster, but less secure, Random
|
54
|
+
# KSUID.configure do |config|
|
55
|
+
# config.random_generator = -> { Random.new.bytes(KSUID::BYTES[:payload]) }
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# @param generator [#call] a callable that returns 16 bytes
|
59
|
+
# @return [#call] a callable that returns 16 bytes
|
60
|
+
def random_generator=(generator)
|
61
|
+
assert_generator_is_callable(generator)
|
62
|
+
assert_payload_size(generator)
|
63
|
+
|
64
|
+
@random_generator = generator
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Raises an error if the assigned generator is not callable
|
70
|
+
#
|
71
|
+
# @api private
|
72
|
+
#
|
73
|
+
# @raise [ConfigurationError] if the generator is not callable
|
74
|
+
# @return [nil]
|
75
|
+
def assert_generator_is_callable(generator)
|
76
|
+
return if generator.respond_to?(:call)
|
77
|
+
|
78
|
+
raise ConfigurationError, "Random generator #{generator} is not callable"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Raises an error if the assigned generator generates the wrong size
|
82
|
+
#
|
83
|
+
# @api private
|
84
|
+
#
|
85
|
+
# @raise [ConfigurationError] if the generator generates the wrong size payload
|
86
|
+
# @return [nil]
|
87
|
+
def assert_payload_size(generator)
|
88
|
+
return if (length = generator.call.length) == (expected_length = BYTES[:payload])
|
89
|
+
|
90
|
+
raise ConfigurationError, 'Random generator generates the wrong number of bytes ' \
|
91
|
+
"(#{length} generated, #{expected_length} expected)"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KSUID
|
4
|
+
# Enables the usage of KSUID types within ActiveRecord when Rails is loaded
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
class Railtie < ::Rails::Railtie
|
8
|
+
initializer 'ksuid' do
|
9
|
+
ActiveSupport.on_load :active_record do
|
10
|
+
require 'ksuid/activerecord'
|
11
|
+
require 'ksuid/activerecord/table_definition'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/ksuid/type.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'securerandom'
|
4
3
|
require_relative 'base62'
|
5
4
|
require_relative 'utils'
|
6
5
|
|
@@ -31,11 +30,11 @@ module KSUID
|
|
31
30
|
# @example Generate a new KSUID for a given timestamp
|
32
31
|
# KSUID::Type.new(time: Time.parse('2017-11-05 15:00:04 UTC'))
|
33
32
|
#
|
34
|
-
# @param payload [String
|
33
|
+
# @param payload [String, Array<Integer>, nil] the payload for the KSUID
|
35
34
|
# @param time [Time] the timestamp to use for the KSUID
|
36
35
|
# @return [KSUID::Type] the generated KSUID
|
37
36
|
def initialize(payload: nil, time: Time.now)
|
38
|
-
payload ||=
|
37
|
+
payload ||= KSUID.config.random_generator.call
|
39
38
|
byte_encoding = Utils.int_to_bytes(time.to_i - EPOCH_TIME)
|
40
39
|
|
41
40
|
@uid = byte_encoding.bytes + payload.bytes
|
@@ -64,6 +63,18 @@ module KSUID
|
|
64
63
|
other.to_s == to_s
|
65
64
|
end
|
66
65
|
|
66
|
+
# Prints the KSUID for debugging within a console
|
67
|
+
#
|
68
|
+
# @api public
|
69
|
+
#
|
70
|
+
# @example Show the maximum KSUID
|
71
|
+
# KSUID.max.inspect #=> "<KSUID(aWgEPTl1tmebfsQzFP4bxwgy80V)>"
|
72
|
+
#
|
73
|
+
# @return [String]
|
74
|
+
def inspect
|
75
|
+
"<KSUID(#{self})>"
|
76
|
+
end
|
77
|
+
|
67
78
|
# The payload for the KSUID, as a hex-encoded string
|
68
79
|
#
|
69
80
|
# This is generally useful for comparing against the Go tool
|
@@ -121,9 +132,7 @@ module KSUID
|
|
121
132
|
#
|
122
133
|
# @return [Integer] the Unix timestamp for the event (without the epoch shift)
|
123
134
|
def to_i
|
124
|
-
|
125
|
-
|
126
|
-
unix_time
|
135
|
+
Utils.int_from_bytes(uid.first(BYTES[:timestamp]))
|
127
136
|
end
|
128
137
|
|
129
138
|
# The KSUID as a base 62-encoded string
|
data/lib/ksuid/utils.rb
CHANGED
@@ -15,20 +15,19 @@ module KSUID
|
|
15
15
|
|
16
16
|
# Converts a byte string or byte array into a hex-encoded string
|
17
17
|
#
|
18
|
-
# @param bytes [String
|
18
|
+
# @param bytes [String, Array<Integer>] the byte string or array
|
19
19
|
# @return [String] the byte string as a hex-encoded string
|
20
20
|
def self.bytes_to_hex_string(bytes)
|
21
21
|
bytes = bytes.bytes if bytes.is_a?(String)
|
22
22
|
|
23
23
|
byte_string_from_array(bytes)
|
24
|
-
.
|
25
|
-
.first
|
24
|
+
.unpack1('H*')
|
26
25
|
.upcase
|
27
26
|
end
|
28
27
|
|
29
28
|
# Converts a byte string or byte array into an integer
|
30
29
|
#
|
31
|
-
# @param bytes [String
|
30
|
+
# @param bytes [String, Array<Integer>] the byte string or array
|
32
31
|
# @return [Integer] the resulting integer
|
33
32
|
def self.int_from_bytes(bytes)
|
34
33
|
bytes = bytes.bytes if bytes.is_a?(String)
|
data/lib/ksuid/version.rb
CHANGED
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ksuid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Herold
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-11-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.15'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.15'
|
27
27
|
description: Ruby implementation of the K-Sortable Unique IDentifier
|
@@ -37,7 +37,13 @@ files:
|
|
37
37
|
- README.md
|
38
38
|
- ksuid.gemspec
|
39
39
|
- lib/ksuid.rb
|
40
|
+
- lib/ksuid/activerecord.rb
|
41
|
+
- lib/ksuid/activerecord/binary_type.rb
|
42
|
+
- lib/ksuid/activerecord/table_definition.rb
|
43
|
+
- lib/ksuid/activerecord/type.rb
|
40
44
|
- lib/ksuid/base62.rb
|
45
|
+
- lib/ksuid/configuration.rb
|
46
|
+
- lib/ksuid/railtie.rb
|
41
47
|
- lib/ksuid/type.rb
|
42
48
|
- lib/ksuid/utils.rb
|
43
49
|
- lib/ksuid/version.rb
|
@@ -45,7 +51,7 @@ homepage: https://github.com/michaelherold/ksuid
|
|
45
51
|
licenses:
|
46
52
|
- MIT
|
47
53
|
metadata: {}
|
48
|
-
post_install_message:
|
54
|
+
post_install_message:
|
49
55
|
rdoc_options: []
|
50
56
|
require_paths:
|
51
57
|
- lib
|
@@ -60,10 +66,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
66
|
- !ruby/object:Gem::Version
|
61
67
|
version: '0'
|
62
68
|
requirements: []
|
63
|
-
|
64
|
-
|
65
|
-
signing_key:
|
69
|
+
rubygems_version: 3.0.6
|
70
|
+
signing_key:
|
66
71
|
specification_version: 4
|
67
72
|
summary: Ruby implementation of the K-Sortable Unique IDentifier
|
68
73
|
test_files: []
|
69
|
-
has_rdoc:
|