ksuid 0.1.0 → 0.4.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
- SHA1:
3
- metadata.gz: d7e3d2e87bfbd69ce702f4dcc0d7067394f39fe7
4
- data.tar.gz: 8159913d3848e49c9e41143db7e1fc4fb115deee
2
+ SHA256:
3
+ metadata.gz: 65b5bd73fb4864bb0df8cf57c1214622325afa077e031baffb2a1e73b2820f2c
4
+ data.tar.gz: 55245cb9fba9e57c9bf115f189fb954f2e1b08604e89ba8b5c9aeea73967e5ba
5
5
  SHA512:
6
- metadata.gz: a298ed92c64c0398c14853de8d246fc0e78b29aca724dde716fe8c67640e37621db09f62f4a76da86de0576d64a2be7d8265f1cdb692056244a0838efe9a1da9
7
- data.tar.gz: 5282c1bfe4bc4a09761510fd4a330c7e8429626e67c245e62af3d41b7b391be2470c598fa91aaf6a5ebe7df51a0aec8a0e3a7f46ac01afc5afbbf733e51e1dd8
6
+ metadata.gz: af5cc8ed5f9d81616fd61e6f03690c841489e9f2c2d28d892137c1eb410ec88e61540cd3dd10d5ac0d0439f9b9ecd9afdc00cceae61276cca521d5436c159e29
7
+ data.tar.gz: 13479dc7d24cf9422c6bc3ca705c6c777abf121f5ade7a9f9c8124c7b9f8034844d8707b2f8265c18178b9b16b50be28a3c1894888d97fe7f42aa1e981d11a97
data/CHANGELOG.md CHANGED
@@ -4,12 +4,38 @@ 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] - 2017-11-05
7
+ ## [0.4.0](https://github.com/michaelherold/ksuid/compare/v0.3.0...v0.4.0) - 2022-07-29
8
+
9
+ ### Added
10
+
11
+ - `KSUID::Type` acts as a proper value object now, which means that you may use it as a Hash key or use it in ActiveRecord's `.includes`. `KSUID::Type#eql?`, `KSUID::Type#hash`, and `KSUID::Type#==` now work as expected. Note that `KSUID::Type#==` is more lax than `KSUID::Type#eql?` because it can also match any object that converts to a string matching its value. This means that you can use it to match against `String` KSUIDs.
12
+
13
+ ### Fixed
14
+
15
+ - `ActiveRecord::QueryMethods#include` works as expected now due to the fix on the value object semantics of `KSUID::Type`.
16
+ - Binary KSUID primary and foreign keys work as expected on JRuby.
17
+
18
+ ## [0.3.0](https://github.com/michaelherold/ksuid/compare/v0.2.0...v0.3.0) - 2021-10-07
19
+
20
+ ### Added
21
+
22
+ - A utility function for converting from a hexidecimal-encoded string to a byte string. This is necessary to handle the default encoding of binary fields within PostgreSQL.
23
+
24
+ ## [0.2.0](https://github.com/michaelherold/ksuid/compare/v0.1.0...v0.2.0) - 2020-11-11
25
+
26
+ ### Added
27
+
28
+ - 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.
29
+ - 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.
30
+
31
+ ### Changed
32
+
33
+ - The `KSUID::Type#inspect` method now makes it much easier to see what you're looking at in the console when you're debugging.
34
+
35
+ ## [0.1.0](https://github.com/michaelherold/ksuid/tree/v0.1.0) - 2017-11-05
8
36
 
9
37
  ### Added
10
38
 
11
39
  - Basic `KSUID.new` interface.
12
40
  - Parsing of bytes through `KSUID.from_bytes`.
13
41
  - 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 `bundle exec rake spec`. If your specs pass, return to step 3.
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 `bundle exec rake`. If your specs or any of the linters fail, return to step 5.
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. Run `bundle exec inch`. If your changes are below a B in documentation, go back to step 8.
37
- 10. Commit and push your changes.
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
@@ -1,14 +1,14 @@
1
1
  # KSUID for Ruby
2
2
 
3
- [![Build Status](https://travis-ci.org/michaelherold/ksuid-ruby.svg)][travis]
3
+ [![Build Status](https://github.com/michaelherold/ksuid-ruby/workflows/Continuous%20integration/badge.svg)][actions]
4
4
  [![Test Coverage](https://api.codeclimate.com/v1/badges/94b2a2d4082bff21c10f/test_coverage)][test-coverage]
5
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/94b2a2d4082bff21c10f/maintainability)][maintainability]
6
6
  [![Inline docs](http://inch-ci.org/github/michaelherold/ksuid-ruby.svg?branch=master)][inch]
7
7
 
8
+ [actions]: https://github.com/michaelherold/ksuid-ruby/actions
8
9
  [inch]: http://inch-ci.org/github/michaelherold/ksuid-ruby
9
10
  [maintainability]: https://codeclimate.com/github/michaelherold/ksuid-ruby/maintainability
10
11
  [test-coverage]: https://codeclimate.com/github/michaelherold/ksuid-ruby/test_coverage
11
- [travis]: https://travis-ci.org/michaelherold/ksuid-ruby
12
12
 
13
13
  ksuid is a Ruby library that can generate and parse [KSUIDs](https://github.com/segmentio/ksuid). The original readme for the Go version of KSUID does a great job of explaining what they are and how they should be used, so it is excerpted here.
14
14
 
@@ -92,17 +92,135 @@ 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.
98
215
 
99
216
  ## Supported Ruby Versions
100
217
 
101
- This library aims to support and is [tested against][travis] the following Ruby versions:
218
+ This library aims to support and is [tested against][actions] the following Ruby versions:
102
219
 
103
- * Ruby 2.3
104
- * Ruby 2.4
105
- * JRuby 9.1
220
+ * Ruby 2.5
221
+ * Ruby 2.6
222
+ * Ruby 2.7
223
+ * JRuby 9.2
106
224
 
107
225
  If something doesn't work on one of these versions, it's a bug.
108
226
 
data/ksuid.gemspec CHANGED
@@ -6,11 +6,11 @@ Gem::Specification.new do |spec|
6
6
  spec.name = 'ksuid'
7
7
  spec.version = KSUID::VERSION
8
8
  spec.authors = ['Michael Herold']
9
- spec.email = ['michael@michaeljherold.com']
9
+ spec.email = ['opensource@michaeljherold.com']
10
10
 
11
11
  spec.summary = 'Ruby implementation of the K-Sortable Unique IDentifier'
12
12
  spec.description = spec.summary
13
- spec.homepage = 'https://github.com/michaelherold/ksuid'
13
+ spec.homepage = 'https://github.com/michaelherold/ksuid-ruby'
14
14
  spec.license = 'MIT'
15
15
 
16
16
  spec.files = %w[CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md]
@@ -18,5 +18,5 @@ Gem::Specification.new do |spec|
18
18
  spec.files += Dir['lib/**/*.rb']
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_development_dependency 'bundler', '~> 1.15'
21
+ spec.add_development_dependency 'bundler', '>= 1.15'
22
22
  end
@@ -0,0 +1,58 @@
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
+ end
55
+ end
56
+ end
57
+
58
+ 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,57 @@
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
+ end
54
+ end
55
+ end
56
+
57
+ ActiveRecord::Type.register(:ksuid, KSUID::ActiveRecord::Type)
@@ -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
data/lib/ksuid/base62.rb CHANGED
@@ -20,6 +20,24 @@ module KSUID
20
20
  # @api private
21
21
  BASE = CHARSET.size
22
22
 
23
+ # A matcher that checks whether a String has a character outside the charset
24
+ #
25
+ # @api private
26
+ MATCHER = /[^#{CHARSET}]/.freeze
27
+
28
+ # Checks whether a string is a base 62-compatible string
29
+ #
30
+ # @api public
31
+ #
32
+ # @example Checks a KSUID for base 62 compatibility
33
+ # KSUID::Base62.compatible?("15Ew2nYeRDscBipuJicYjl970D1") #=> true
34
+ #
35
+ # @param string [String] the string to check for compatibility
36
+ # @return [Boolean]
37
+ def self.compatible?(string)
38
+ string.each_char.all? { |char| !MATCHER.match?(char) }
39
+ end
40
+
23
41
  # Decodes a base 62-encoded string into an integer
24
42
  #
25
43
  # @api public
@@ -71,7 +89,7 @@ module KSUID
71
89
  # 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
72
90
  # )
73
91
  #
74
- # @param bytes [String|Array<Integer>] the bytes to encode
92
+ # @param bytes [String, Array<Integer>] the bytes to encode
75
93
  # @return [String] the encoded bytes as a base 62 string
76
94
  def self.encode_bytes(bytes)
77
95
  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|Array<Integer>|nil] the payload for the KSUID
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 ||= SecureRandom.random_bytes(BYTES[: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,48 @@ module KSUID
64
63
  other.to_s == to_s
65
64
  end
66
65
 
66
+ # Checks whether this KSUID hashes to the same hash key as another
67
+ #
68
+ # @api semipublic
69
+ #
70
+ # @example Checks whether two KSUIDs hash to the same key
71
+ # KSUID.new.eql? KSUID.new
72
+ #
73
+ # @param other [KSUID::Type] the other KSUID to check against
74
+ # @return [Boolean]
75
+ def eql?(other)
76
+ hash == other.hash
77
+ end
78
+
79
+ # Generates the key to use when using a KSUID as a hash key
80
+ #
81
+ # @api semipublic
82
+ #
83
+ # @example Using a KSUID as a Hash key
84
+ # ksuid1 = KSUID.new
85
+ # ksuid2 = KSUID.from_base62(ksuid1.to_s)
86
+ # values_by_ksuid = {}
87
+ #
88
+ # values_by_ksuid[ksuid1] = "example"
89
+ # values_by_ksuid[ksuid2] #=> "example"
90
+ #
91
+ # @return [Integer]
92
+ def hash
93
+ @uid.hash
94
+ end
95
+
96
+ # Prints the KSUID for debugging within a console
97
+ #
98
+ # @api public
99
+ #
100
+ # @example Show the maximum KSUID
101
+ # KSUID.max.inspect #=> "<KSUID(aWgEPTl1tmebfsQzFP4bxwgy80V)>"
102
+ #
103
+ # @return [String]
104
+ def inspect
105
+ "<KSUID(#{self})>"
106
+ end
107
+
67
108
  # The payload for the KSUID, as a hex-encoded string
68
109
  #
69
110
  # This is generally useful for comparing against the Go tool
@@ -121,9 +162,7 @@ module KSUID
121
162
  #
122
163
  # @return [Integer] the Unix timestamp for the event (without the epoch shift)
123
164
  def to_i
124
- unix_time = Utils.int_from_bytes(uid.first(BYTES[:timestamp]))
125
-
126
- unix_time
165
+ Utils.int_from_bytes(uid.first(BYTES[:timestamp]))
127
166
  end
128
167
 
129
168
  # The KSUID as a base 62-encoded string
data/lib/ksuid/utils.rb CHANGED
@@ -5,7 +5,19 @@ module KSUID
5
5
  #
6
6
  # @api private
7
7
  module Utils
8
- # Converts a byte string into a byte array
8
+ # A regular expression for splitting bytes out of a "binary" string
9
+ #
10
+ # @api private
11
+ # @return [Regexp] the splitter
12
+ BYTES = /.{8}/.freeze
13
+
14
+ # A regular expression for splitting a String into pairs of characters
15
+ #
16
+ # @api private
17
+ # @return [Regexp] the splitter
18
+ PAIRS = /.{2}/.freeze
19
+
20
+ # Converts a byte array into a byte string
9
21
  #
10
22
  # @param bytes [String] a byte string
11
23
  # @return [Array<Integer>] an array of bytes from the byte string
@@ -13,22 +25,36 @@ module KSUID
13
25
  bytes.pack('C*')
14
26
  end
15
27
 
28
+ # Converts a hex string into a byte string
29
+ #
30
+ # @param hex [String] a hex-encoded KSUID
31
+ # @param bits [Integer] the expected number of bits for the result
32
+ # @return [String] the byte string
33
+ def self.byte_string_from_hex(hex, bits = 32)
34
+ byte_array =
35
+ hex
36
+ .rjust(bits, '0')
37
+ .scan(PAIRS)
38
+ .map { |bytes| bytes.to_i(16) }
39
+
40
+ byte_string_from_array(byte_array)
41
+ end
42
+
16
43
  # Converts a byte string or byte array into a hex-encoded string
17
44
  #
18
- # @param bytes [String|Array<Integer>] the byte string or array
45
+ # @param bytes [String, Array<Integer>] the byte string or array
19
46
  # @return [String] the byte string as a hex-encoded string
20
47
  def self.bytes_to_hex_string(bytes)
21
48
  bytes = bytes.bytes if bytes.is_a?(String)
22
49
 
23
50
  byte_string_from_array(bytes)
24
- .unpack('H*')
25
- .first
51
+ .unpack1('H*')
26
52
  .upcase
27
53
  end
28
54
 
29
55
  # Converts a byte string or byte array into an integer
30
56
  #
31
- # @param bytes [String|Array<Integer>] the byte string or array
57
+ # @param bytes [String, Array<Integer>] the byte string or array
32
58
  # @return [Integer] the resulting integer
33
59
  def self.int_from_bytes(bytes)
34
60
  bytes = bytes.bytes if bytes.is_a?(String)
@@ -48,9 +74,8 @@ module KSUID
48
74
  int
49
75
  .to_s(2)
50
76
  .rjust(bits, '0')
51
- .split('')
52
- .each_slice(8)
53
- .map { |digits| digits.join.to_i(2) }
77
+ .scan(BYTES)
78
+ .map { |digits| digits.to_i(2) }
54
79
  .pack("C#{bits / 8}")
55
80
  end
56
81
  end
data/lib/ksuid/version.rb CHANGED
@@ -4,5 +4,5 @@ module KSUID
4
4
  # The version of the KSUID gem
5
5
  #
6
6
  # @return [String]
7
- VERSION = '0.1.0'
7
+ VERSION = '0.4.0'
8
8
  end
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|Array<Integer>] the byte string or array to convert into an object
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|Array<Integer>|nil] the payload for the KSUID
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)
metadata CHANGED
@@ -1,32 +1,32 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ksuid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.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: 2017-11-05 00:00:00.000000000 Z
11
+ date: 2022-07-29 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
28
28
  email:
29
- - michael@michaeljherold.com
29
+ - opensource@michaeljherold.com
30
30
  executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
@@ -37,15 +37,21 @@ 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
44
- homepage: https://github.com/michaelherold/ksuid
50
+ homepage: https://github.com/michaelherold/ksuid-ruby
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
- rubyforge_project:
64
- rubygems_version: 2.6.13
65
- signing_key:
69
+ rubygems_version: 3.1.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: