ksuid 0.4.0 → 1.0.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
  SHA256:
3
- metadata.gz: 65b5bd73fb4864bb0df8cf57c1214622325afa077e031baffb2a1e73b2820f2c
4
- data.tar.gz: 55245cb9fba9e57c9bf115f189fb954f2e1b08604e89ba8b5c9aeea73967e5ba
3
+ metadata.gz: c97a36378134ab6cdd674866a6744e5b84ff5976db6575d5364cc844cc9dbf4f
4
+ data.tar.gz: c5ebfa42d22044e518a63f9508924517a5b7ec83a9e3059c64ecb8075ee88e07
5
5
  SHA512:
6
- metadata.gz: af5cc8ed5f9d81616fd61e6f03690c841489e9f2c2d28d892137c1eb410ec88e61540cd3dd10d5ac0d0439f9b9ecd9afdc00cceae61276cca521d5436c159e29
7
- data.tar.gz: 13479dc7d24cf9422c6bc3ca705c6c777abf121f5ade7a9f9c8124c7b9f8034844d8707b2f8265c18178b9b16b50be28a3c1894888d97fe7f42aa1e981d11a97
6
+ metadata.gz: 6743031c8702c60e3be6d58f2bde58b1290abc1943afb110adf3a9a2066017d24a453a826373312539175629b2ed9a612ed257abdad22e0cef74a88e045a12e5
7
+ data.tar.gz: c2c504f17a31fe7d0c25475b606045424868aca2aa0015e78ff41da89828a7e1eb5b88bcf88a8e0da34a81945d5c68b3cbfb1d4acead1d9227c423cc89ece0ae
data/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ 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
+ ## [1.0.0](https://github.com/michaelherold/ksuid/compare/v0.5.0...v1.0.0) - 2023-02-25
8
+
9
+ ### Removed
10
+
11
+ - Extracted the ActiveRecord functionality into its own gem, `activerecord-ksuid`. It is API-compatible with v0.5.0 so to restore functionality, you should only need to add the new gem to your application. See [the upgrading notice](./UPGRADING.md) for more information.
12
+
13
+ ## [0.5.0](https://github.com/michaelherold/ksuid/compare/v0.4.0...v0.5.0) - 2022-08-18
14
+
15
+ ### Added
16
+
17
+ - If you'd rather deal in KSUID strings instead of `KSUID::Type`s, you can now generate them simply with `KSUID.string`. It takes the same arguments, `payload` and `time` as `KSUID.new`, but returns a string instead of a `KSUID::Type`.
18
+ - `KSUID.prefixed` and the `KSUID::Prefixed` class now can generate prefixed KSUIDs to make them visually identifiable for their source. You cannot prefix a binary-encoded KSUID, only base 62-encoded ones.
19
+ - `ActiveRecord::KSUID` now accepts a `prefix:` argument for handling prefixed KSUIDs. In addition, the `ksuid` column type also accepts a `prefix:` argument to calculate the intended size of the column with the prefix.
20
+
21
+ ### Deprecated
22
+
23
+ - `KSUID::ActiveRecord` is now `ActiveRecord::KSUID`. The original constant will continue to work until v1.0.0, but will emit a warning upon boot of your application. To silence the deprecation, change all uses of `KSUID::ActiveRecord` to the new constant, `ActiveRecord::KSUID`. See the [upgrading notice][./UPGRADING.md] for more information.
24
+
25
+ ### Miscellaneous
26
+
27
+ - The compatibility check for the Base62 implementation in the gem is about 10x faster now. The original optimization did not optimize as much due to an error with the benchmark. This change has a tested benchmark that shows a great improvement. Note that this is a micro-optimization and we see no real performance gain in the parsing of KSUID strings.
28
+
7
29
  ## [0.4.0](https://github.com/michaelherold/ksuid/compare/v0.3.0...v0.4.0) - 2022-07-29
8
30
 
9
31
  ### Added
data/CONTRIBUTING.md CHANGED
@@ -28,9 +28,9 @@ 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 `appraisal rake spec`. If your specs pass, return to step 3.
31
+ 4. Run `bundle exec rake spec`. If your specs pass, return to step 3.
32
32
  5. Implement your feature or bug fix.
33
- 6. Run `appraisal rake`. If your specs or any of the linters fail, return to step 5.
33
+ 6. Run `bundle exec 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
36
  9. Commit and push your changes.
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # The MIT License (MIT)
2
2
 
3
- Copyright © 2017 Michael Herold
3
+ Copyright © 2017-2022 Michael Herold
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -100,114 +100,43 @@ KSUID.configure do |config|
100
100
  end
101
101
  ```
102
102
 
103
- ### ActiveRecord
103
+ ### Prefixed KSUIDs
104
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:
105
+ If you use KSUIDs in multiple contexts, you can prefix them to make them easily identifiable.
158
106
 
159
107
  ```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
108
+ ksuid = KSUID.prefixed('evt_')
165
109
  ```
166
110
 
167
- You will need to mix in the module into your model as well:
111
+ Just like a normal KSUID, you can use a specific timestamp:
168
112
 
169
- ```ruby
170
- class Event < ApplicationRecord
171
- include KSUID::ActiveRecord[:id]
172
- end
113
+ ``` ruby
114
+ ksuid = KSUID.prefixed('evt_', time: time) # where time is a Time-like object
173
115
  ```
174
116
 
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:
117
+ You can also parse a prefixed KSUID from a string that you received:
178
118
 
179
119
  ```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'
120
+ ksuid = KSUID::Prefixed.from_base62(base62_string, prefix: 'evt_')
184
121
  ```
185
122
 
186
- Once you have required the file(s) that you need, everything else will work as it does above.
123
+ Prefixed KSUIDs order themselves with non-prefixed KSUIDs as if their prefix did not exist. With other prefixed KSUIDs, they order first by their prefix, then their timestamp.
187
124
 
188
- #### Binary vs. String KSUIDs
125
+ ### Integrations
189
126
 
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.
127
+ KSUID for Ruby can integrate with other systems through adapter gems. Below is a sample of these adapter gems.
191
128
 
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
- ```
129
+ #### ActiveRecord
199
130
 
200
- #### Use the KSUID as your `created_at` timestamp
131
+ If you want to include KSUID columns in your ActiveRecord models, install the `activerecord-ksuid` gem. If you are using it within a Rails app, run the following:
201
132
 
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:
133
+ bundle add activerecord-ksuid --require active_record/ksuid/railtie
134
+
135
+ If you are using it outside of Rails, add this to your Gemfile:
203
136
 
204
- ```ruby
205
- class Event < ApplicationRecord
206
- include KSUID::ActiveRecord[:my_field_name, created_at: true]
207
- end
208
- ```
137
+ gem 'activerecord-ksuid', require: ['ksuid', 'active_record/ksuid', 'active_record/ksuid/table_definition']
209
138
 
210
- This allows you to be efficient in your database design if that is a constraint you need to satisfy.
139
+ See [the readme for the integration](https://github.com/michaelherold/ksuid-ruby/blob/main/activerecord-ksuid/README.md) for more information.
211
140
 
212
141
  ## Contributing
213
142
 
@@ -217,10 +146,10 @@ So you’re interested in contributing to KSUID? Check out our [contributing gui
217
146
 
218
147
  This library aims to support and is [tested against][actions] the following Ruby versions:
219
148
 
220
- * Ruby 2.5
221
- * Ruby 2.6
222
149
  * Ruby 2.7
223
- * JRuby 9.2
150
+ * Ruby 3.0
151
+ * Ruby 3.1
152
+ * JRuby 9.3
224
153
 
225
154
  If something doesn't work on one of these versions, it's a bug.
226
155
 
data/UPGRADING.md ADDED
@@ -0,0 +1,26 @@
1
+ # Upgrading instructions for KSUID for Ruby
2
+
3
+ ## v1.0.0
4
+
5
+ ### Extracted `ActiveRecord::KSUID` into its own gem
6
+
7
+ That KSUID for Ruby included ActiveRecord support directly in its gem has always been a regret of mine. It adds ActiveRecord and Rails concerns to a gem that you can use in any context. It makes running the test suite more complicated for no real gain. And it makes it kludgy to add support for more systems, like Sequel, since you have conflicting concerns in the same gem.
8
+
9
+ To remove this problem, v1.0.0 extracts the ActiveRecord behavior into its own gem, `activerecord-ksuid`. This version is a straight extraction with an improved test suite so it _should_ mean that the only change you have to make when upgrading from v0.5.0 is to do the following in your Gemfile:
10
+
11
+ ```diff
12
+ - gem 'ksuid'
13
+ + gem 'activerecord-ksuid'
14
+ ```
15
+
16
+ If you are still on a version prior to v0.5.0, upgrade to that version first, solve the deprecation notice below, ensure your app still works, and then upgrade to v1.0.0.
17
+
18
+ ## v0.5.0
19
+
20
+ ### Deprecated `KSUID::ActiveRecord` in favor of `ActiveRecord::KSUID`
21
+
22
+ This version deprecates the original constant for the ActiveRecord integration, `KSUID::ActiveRecord`. This change is in preparation for extracting the ActiveRecord integration into its own gem. Continuing to use the original constant will show deprecation warnings upon boot of your application.
23
+
24
+ Migrating for this version should be quick: simply do a global replace of `KSUID::ActiveRecord` for `ActiveRecord::KSUID`. No other changes should be necessary.
25
+
26
+ In the future release of v1.0.0, you will need to also include `activerecord-ksuid` your Gemfile. This gem is as-yet unreleased, with a release intended concurrently with v1.0.0 of KSUID for Ruby.
data/ksuid.gemspec CHANGED
@@ -13,10 +13,12 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = 'https://github.com/michaelherold/ksuid-ruby'
14
14
  spec.license = 'MIT'
15
15
 
16
- spec.files = %w[CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md]
16
+ spec.files = %w[CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md UPGRADING.md]
17
17
  spec.files += %w[ksuid.gemspec]
18
18
  spec.files += Dir['lib/**/*.rb']
19
19
  spec.require_paths = ['lib']
20
20
 
21
+ spec.metadata['rubygems_mfa_required'] = 'true'
22
+
21
23
  spec.add_development_dependency 'bundler', '>= 1.15'
22
24
  end
data/lib/ksuid/base62.rb CHANGED
@@ -35,7 +35,7 @@ module KSUID
35
35
  # @param string [String] the string to check for compatibility
36
36
  # @return [Boolean]
37
37
  def self.compatible?(string)
38
- string.each_char.all? { |char| !MATCHER.match?(char) }
38
+ !MATCHER.match?(string)
39
39
  end
40
40
 
41
41
  # Decodes a base 62-encoded string into an integer
@@ -51,12 +51,12 @@ module KSUID
51
51
  def self.decode(ksuid)
52
52
  result = 0
53
53
 
54
- ksuid.split('').each_with_index do |char, position|
54
+ ksuid.chars.each_with_index do |char, position|
55
55
  unless (digit = CHARSET.index(char))
56
56
  raise(ArgumentError, "#{ksuid} is not a base 62 number")
57
57
  end
58
58
 
59
- result += digit * BASE**(ksuid.length - (position + 1))
59
+ result += digit * (BASE**(ksuid.length - (position + 1)))
60
60
  end
61
61
 
62
62
  result
@@ -76,7 +76,7 @@ module KSUID
76
76
  chars = encode_without_padding(number)
77
77
 
78
78
  chars << padding if chars.empty?
79
- chars.reverse.join('').rjust(STRING_LENGTH, padding)
79
+ chars.reverse.join.rjust(STRING_LENGTH, padding)
80
80
  end
81
81
 
82
82
  # Encodes a byte string or byte array into base 62
@@ -87,8 +87,11 @@ module KSUID
87
87
  def assert_payload_size(generator)
88
88
  return if (length = generator.call.length) == (expected_length = BYTES[:payload])
89
89
 
90
- raise ConfigurationError, 'Random generator generates the wrong number of bytes ' \
90
+ raise(
91
+ ConfigurationError,
92
+ 'Random generator generates the wrong number of bytes ' \
91
93
  "(#{length} generated, #{expected_length} expected)"
94
+ )
92
95
  end
93
96
  end
94
97
  end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KSUID
4
+ # Encapsulates the data type for a prefixed KSUID
5
+ #
6
+ # When you have different types of KSUIDs in your application, it can be
7
+ # helpful to add an identifier to the front of them to give you an idea for
8
+ # what kind of object the KSUID belongs to.
9
+ #
10
+ # For example, you might use KSUIDs to identify both Events and Customers. For
11
+ # an Event, you could prefix the KSUID with the string `evt_`. Likewise, for
12
+ # Customers, you could prefix them with the string `cus_`.
13
+ #
14
+ # {KSUID::Prefixed} gives you affordances for doing just this.
15
+ #
16
+ # ## Ordering
17
+ #
18
+ # {KSUID::Prefixed}s are partially orderable with {KSUID::Type} by their
19
+ # timestamps. When ordering them with other {KSUID::Prefixed} instances, they
20
+ # order first by prefix, then by timestamp. This means that in a mixed
21
+ # collection, all Customer KSUIDs (prefix: `cus_`) would be grouped before all
22
+ # Event KSUIDs (prefix `evt_`).
23
+ #
24
+ # ## Interface
25
+ #
26
+ # You typically will not instantiate this class directly, but instead use the
27
+ # {KSUID.prefixed} builder method to save some typing.
28
+ #
29
+ # The most commonly used helper methods for the {KSUID} module also exist on
30
+ # {KSUID::Prefixed} for converting between different forms of output.
31
+ #
32
+ # ## Differences from {KSUID::Type}
33
+ #
34
+ # One other thing to note is that {KSUID::Prefixed} is not intended to handle
35
+ # binary data because the prefix does not make sense in either the byte string
36
+ # or packed array formats.
37
+ #
38
+ # @since 0.5.0
39
+ class Prefixed < Type
40
+ include Comparable
41
+
42
+ # Converts a KSUID-compatible value into a {KSUID::Prefixed}
43
+ #
44
+ # @api public
45
+ #
46
+ # @example Converts a base 62 KSUID string into a {KSUID::Prefixed}
47
+ # KSUID::Prefixed.call('15Ew2nYeRDscBipuJicYjl970D1', prefix: 'evt_')
48
+ #
49
+ # @param ksuid [String, KSUID::Prefixed, KSUID::Type] the prefixed KSUID-compatible value
50
+ # @return [KSUID::Prefixed] the converted, prefixed KSUID
51
+ # @raise [ArgumentError] if the value is not prefixed KSUID-compatible
52
+ def self.call(ksuid, prefix:)
53
+ return unless ksuid && prefix
54
+
55
+ case ksuid
56
+ when KSUID::Prefixed then from_base62(ksuid.to_ksuid.to_s, prefix: prefix)
57
+ when KSUID::Type then from_base62(ksuid.to_s, prefix: prefix)
58
+ when String then cast_string(ksuid, prefix: prefix)
59
+ else
60
+ raise ArgumentError, "Cannot convert #{ksuid.inspect} to KSUID::Prefixed"
61
+ end
62
+ end
63
+
64
+ # Converts a base 62-encoded string into a {KSUID::Prefixed}
65
+ #
66
+ # @api public
67
+ #
68
+ # @example Parse a KSUID string into a prefixed object
69
+ # KSUID::Prefixed.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW', prefix: 'evt_')
70
+ #
71
+ # @param string [String] the base 62-encoded KSUID to convert into an object
72
+ # @param prefix [String] the prefix to add to the KSUID
73
+ # @return [KSUID::Prefixed] the prefixed KSUID generated from the string
74
+ def self.from_base62(string, prefix:)
75
+ string = string.sub(/\A#{prefix}/, '')
76
+ int = Base62.decode(string)
77
+ bytes = Utils.int_to_bytes(int, 160)
78
+
79
+ from_bytes(bytes, prefix: prefix)
80
+ end
81
+
82
+ # Casts a string into a {KSUID::Prefixed}
83
+ #
84
+ # @api private
85
+ #
86
+ # @param ksuid [String] the string to convert into a {KSUID::Prefixed}
87
+ # @param prefix [String] the prefix to prepend to the KSUID
88
+ # @return [KSUID::Prefixed] the converted, prefixed KSUID
89
+ def self.cast_string(ksuid, prefix:)
90
+ ksuid = ksuid[-KSUID::BYTES[:base62]..-1] if ksuid.length >= KSUID::BYTES[:base62]
91
+
92
+ unless Base62.compatible?(ksuid)
93
+ raise ArgumentError, 'Prefixed KSUIDs cannot be binary strings'
94
+ end
95
+
96
+ from_base62(ksuid, prefix: prefix)
97
+ end
98
+ private_class_method :cast_string
99
+
100
+ # Converts a byte string or byte array into a KSUID
101
+ #
102
+ # @api private
103
+ #
104
+ # @param bytes [String] the byte string to convert into an object
105
+ # @return [KSUID::Prefixed] the prefixed KSUID generated from the bytes
106
+ def self.from_bytes(bytes, prefix:)
107
+ bytes = bytes.bytes
108
+ timestamp = Utils.int_from_bytes(bytes.first(KSUID::BYTES[:timestamp]))
109
+ payload = Utils.byte_string_from_array(bytes.last(KSUID::BYTES[:payload]))
110
+
111
+ new(prefix, payload: payload, time: Time.at(timestamp + EPOCH_TIME))
112
+ end
113
+ private_class_method :from_bytes
114
+
115
+ # Instantiates a new {KSUID::Prefixed}
116
+ #
117
+ # @api semipublic
118
+ #
119
+ # @example Generate a new {KSUID::Prefixed} for the current second
120
+ # KSUID::Prefixed.new('evt_')
121
+ #
122
+ # @example Generate a new {KSUID::Prefixed} for a given timestamp
123
+ # KSUID::Prefixed.new('cus_', time: Time.parse('2017-11-05 15:00:04 UTC'))
124
+ #
125
+ # @param prefix [String] the prefix to add to the KSUID
126
+ # @param payload [String, Array<Integer>, nil] the payload for the KSUID
127
+ # @param time [Time] the timestamp to use for the KSUID
128
+ # @return [KSUID::Prefix] the generated, prefixed KSUID
129
+ def initialize(prefix, payload: nil, time: Time.now)
130
+ raise ArgumentError, 'requires a prefix' unless prefix
131
+
132
+ super(payload: payload, time: time)
133
+
134
+ @prefix = prefix
135
+ end
136
+
137
+ # The prefix in front of the KSUID
138
+ #
139
+ # @api semipublic
140
+ #
141
+ # @example Getting the prefix to create a similar {KSUID::Prefixed}
142
+ # ksuid1 = KSUID.prefixed('cus_')
143
+ # ksuid2 = KSUID.prefixed(ksuid1.prefix)
144
+ #
145
+ # @return [String] the prefix of the {KSUID::Prefixed}
146
+ attr_reader :prefix
147
+
148
+ # Implements the Comparable interface for sorting {KSUID::Prefixed}s
149
+ #
150
+ # @api private
151
+ #
152
+ # @param other [KSUID::Type] the other object to compare against
153
+ # @return [Integer, nil] nil for uncomparable, -1 for less than other,
154
+ # 0 for equal to, 1 for greater than other
155
+ def <=>(other)
156
+ return unless other.is_a?(Type)
157
+ return super if other.instance_of?(Type)
158
+
159
+ if (result = prefix <=> other.prefix).nonzero?
160
+ result
161
+ else
162
+ super
163
+ end
164
+ end
165
+
166
+ # Checks whether this {KSUID::Prefixed} is equal to another
167
+ #
168
+ # @api semipublic
169
+ #
170
+ # @example Checks whether two KSUIDs are equal
171
+ # KSUID.prefixed('evt_') == KSUID.prefixed('evt_')
172
+ #
173
+ # @param other [KSUID::Prefixed] the other {KSUID::Prefixed} to check against
174
+ # @return [Boolean]
175
+ def ==(other)
176
+ other.is_a?(Prefixed) &&
177
+ prefix == other.prefix &&
178
+ super
179
+ end
180
+
181
+ # Generates the key to use when using a {KSUID::Prefixed} as a hash key
182
+ #
183
+ # @api semipublic
184
+ #
185
+ # @example Using a KSUID as a Hash key
186
+ # ksuid1 = KSUID.prefixed('evt_')
187
+ # ksuid2 = ksuid1.dup
188
+ # values_by_ksuid = {}
189
+ #
190
+ # values_by_ksuid[ksuid1] = "example"
191
+ # values_by_ksuid[ksuid2] #=> "example"
192
+ #
193
+ # @return [Integer]
194
+ def hash
195
+ [prefix, @uid].hash
196
+ end
197
+
198
+ # The {KSUID::Prefixed} as a prefixed, hex-encoded string
199
+ #
200
+ # This is generally useful for comparing against the Go tool.
201
+ #
202
+ # @api public
203
+ #
204
+ # @example
205
+ # ksuid = KSUID::Prefixed.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW', prefix: 'evt_')
206
+ #
207
+ # ksuid.raw #=> "evt_0683F789049CC215C099D42B784DBE99341BD79C"
208
+ #
209
+ # @return [String] a prefixed, hex-encoded string
210
+ def raw
211
+ super.prepend(prefix)
212
+ end
213
+
214
+ # Converts the {KSUID::Prefixed} into a {KSUID::Type} by dropping the prefix
215
+ #
216
+ # @api public
217
+ #
218
+ # @example Convert an Event KSUID into a plain KSUID
219
+ # ksuid = KSUID.prefixed('evt_')
220
+ #
221
+ # ksuid.to_ksuid
222
+ #
223
+ # @return [KSUID::Type] the non-prefixed KSUID
224
+ def to_ksuid
225
+ KSUID.from_base62(to_s.sub(/\A#{prefix}/, ''))
226
+ end
227
+
228
+ # The {KSUID::Prefixed} as a base 62-encoded string
229
+ #
230
+ # @api public
231
+ #
232
+ # @example
233
+ # ksuid = KSUID::Prefixed.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW', prefix: 'evt_')
234
+ #
235
+ # ksuid.to_s #=> "evt_0vdbMgWkU6slGpLVCqEFwkkZvuW"
236
+ #
237
+ # @return [String] the prefixed, base 62-encoded string for the {KSUID::Prefixed}
238
+ def to_s
239
+ super.prepend(prefix)
240
+ end
241
+ end
242
+ end
data/lib/ksuid/type.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base62'
4
- require_relative 'utils'
5
-
6
3
  module KSUID
7
4
  # Encapsulates the data type for a KSUID
8
5
  #
data/lib/ksuid/utils.rb CHANGED
@@ -61,7 +61,7 @@ module KSUID
61
61
 
62
62
  bytes
63
63
  .map { |byte| byte.to_s(2).rjust(8, '0') }
64
- .join('')
64
+ .join
65
65
  .to_i(2)
66
66
  end
67
67
 
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.4.0'
7
+ VERSION = '1.0.0'
8
8
  end
data/lib/ksuid.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'ksuid/configuration'
4
- require_relative 'ksuid/type'
5
4
  require_relative 'ksuid/version'
6
5
 
7
6
  # The K-Sortable Unique IDentifier (KSUID)
@@ -41,6 +40,9 @@ require_relative 'ksuid/version'
41
40
  # @example Generate a new KSUID
42
41
  # KSUID.new
43
42
  #
43
+ # @example Generate a KSUID prefixed by `evt_`
44
+ # KSUID.prefixed('evt_')
45
+ #
44
46
  # @example Parse a KSUID string that you have received
45
47
  # KSUID.from_base62('aWgEPTl1tmebfsQzFP4bxwgy80V')
46
48
  #
@@ -63,7 +65,7 @@ module KSUID
63
65
  # The number of bytes that are used to represent each part of a KSUID
64
66
  #
65
67
  # @return [Hash{Symbol => Integer}] the map of data type to number of bytes
66
- BYTES = { payload: 16, timestamp: 4, total: 20 }.freeze
68
+ BYTES = { base62: 27, payload: 16, timestamp: 4, total: 20 }.freeze
67
69
 
68
70
  # The number of characters in a base 62-encoded KSUID
69
71
  #
@@ -75,6 +77,11 @@ module KSUID
75
77
  # @return [String]
76
78
  MAX_STRING_ENCODED = 'aWgEPTl1tmebfsQzFP4bxwgy80V'
77
79
 
80
+ autoload :Base62, 'ksuid/base62'
81
+ autoload :Prefixed, 'ksuid/prefixed'
82
+ autoload :Type, 'ksuid/type'
83
+ autoload :Utils, 'ksuid/utils'
84
+
78
85
  # Converts a KSUID-compatible value into an actual KSUID
79
86
  #
80
87
  # @api public
@@ -89,6 +96,7 @@ module KSUID
89
96
  return unless ksuid
90
97
 
91
98
  case ksuid
99
+ when KSUID::Prefixed then ksuid.to_ksuid
92
100
  when KSUID::Type then ksuid
93
101
  when Array then KSUID.from_bytes(ksuid)
94
102
  when String then cast_string(ksuid)
@@ -190,6 +198,43 @@ module KSUID
190
198
  Type.new(payload: payload, time: time)
191
199
  end
192
200
 
201
+ # Instantiates a new {KSUID::Prefixed}
202
+ #
203
+ # @api public
204
+ # @since 0.5.0
205
+ #
206
+ # @example Generate a new prefixed KSUID for the current second
207
+ # KSUID.prefixed('evt_')
208
+ #
209
+ # @example Generate a new prefixed KSUID for a given timestamp
210
+ # KSUID.prefixed('cus_', time: Time.parse('2022-08-16 10:36:00 UTC'))
211
+ #
212
+ # @param prefix [String] the prefix to apply to the KSUID
213
+ # @param payload [String, Array<Integer>, nil] the payload for the KSUID
214
+ # @param time [Time] the timestamp to use for the KSUID
215
+ # @return [KSUID::Prefixed] the generated, prefixed KSUID
216
+ def self.prefixed(prefix, payload: nil, time: Time.now)
217
+ Prefixed.new(prefix, payload: payload, time: time)
218
+ end
219
+
220
+ # Generates a KSUID string
221
+ #
222
+ # @api public
223
+ # @since 0.5.0
224
+ #
225
+ # @example Generate a new KSUID string for the current second
226
+ # KSUID.string
227
+ #
228
+ # @example Generate a new KSUID string for a given timestamp
229
+ # KSUID.string(time: Time.parse('2017-11-05 15:00:04 UTC'))
230
+ #
231
+ # @param payload [String, Array<Integer>, nil] the payload for the KSUID string
232
+ # @param time [Time] the timestamp to use for the KSUID string
233
+ # @return [String] the generated string
234
+ def self.string(payload: nil, time: Time.now)
235
+ Type.new(payload: payload, time: time).to_s
236
+ end
237
+
193
238
  # Casts a string into a KSUID
194
239
  #
195
240
  # @api private
@@ -205,5 +250,3 @@ module KSUID
205
250
  end
206
251
  private_class_method :cast_string
207
252
  end
208
-
209
- require 'ksuid/railtie' if defined?(Rails)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ksuid
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Herold
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-29 00:00:00.000000000 Z
11
+ date: 2023-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -35,22 +35,20 @@ files:
35
35
  - CONTRIBUTING.md
36
36
  - LICENSE.md
37
37
  - README.md
38
+ - UPGRADING.md
38
39
  - ksuid.gemspec
39
40
  - 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
44
41
  - lib/ksuid/base62.rb
45
42
  - lib/ksuid/configuration.rb
46
- - lib/ksuid/railtie.rb
43
+ - lib/ksuid/prefixed.rb
47
44
  - lib/ksuid/type.rb
48
45
  - lib/ksuid/utils.rb
49
46
  - lib/ksuid/version.rb
50
47
  homepage: https://github.com/michaelherold/ksuid-ruby
51
48
  licenses:
52
49
  - MIT
53
- metadata: {}
50
+ metadata:
51
+ rubygems_mfa_required: 'true'
54
52
  post_install_message:
55
53
  rdoc_options: []
56
54
  require_paths:
@@ -66,7 +64,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
66
64
  - !ruby/object:Gem::Version
67
65
  version: '0'
68
66
  requirements: []
69
- rubygems_version: 3.1.6
67
+ rubygems_version: 3.3.7
70
68
  signing_key:
71
69
  specification_version: 4
72
70
  summary: Ruby implementation of the K-Sortable Unique IDentifier
@@ -1,58 +0,0 @@
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)
@@ -1,56 +0,0 @@
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)
@@ -1,57 +0,0 @@
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)
@@ -1,75 +0,0 @@
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/railtie.rb DELETED
@@ -1,15 +0,0 @@
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