ksuid 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/CONTRIBUTING.md +53 -0
- data/LICENSE.md +20 -0
- data/README.md +124 -0
- data/ksuid.gemspec +22 -0
- data/lib/ksuid.rb +140 -0
- data/lib/ksuid/base62.rb +107 -0
- data/lib/ksuid/type.rb +166 -0
- data/lib/ksuid/utils.rb +57 -0
- data/lib/ksuid/version.rb +8 -0
- metadata +69 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d7e3d2e87bfbd69ce702f4dcc0d7067394f39fe7
|
4
|
+
data.tar.gz: 8159913d3848e49c9e41143db7e1fc4fb115deee
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a298ed92c64c0398c14853de8d246fc0e78b29aca724dde716fe8c67640e37621db09f62f4a76da86de0576d64a2be7d8265f1cdb692056244a0838efe9a1da9
|
7
|
+
data.tar.gz: 5282c1bfe4bc4a09761510fd4a330c7e8429626e67c245e62af3d41b7b391be2470c598fa91aaf6a5ebe7df51a0aec8a0e3a7f46ac01afc5afbbf733e51e1dd8
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
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
|
+
|
7
|
+
## [0.1.0] - 2017-11-05
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- Basic `KSUID.new` interface.
|
12
|
+
- Parsing of bytes through `KSUID.from_bytes`.
|
13
|
+
- 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
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
In the spirit of [free software], **everyone** is encouraged to help improve this project. Here are some ways *you* can contribute:
|
4
|
+
|
5
|
+
* Use alpha, beta, and pre-release versions.
|
6
|
+
* Report bugs.
|
7
|
+
* Suggest new features.
|
8
|
+
* Write or edit documentation.
|
9
|
+
* Write specifications.
|
10
|
+
* Write code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace).
|
11
|
+
* Refactor code.
|
12
|
+
* Fix [issues].
|
13
|
+
* Review patches.
|
14
|
+
|
15
|
+
[free software]: http://www.fsf.org/licensing/essays/free-sw.html
|
16
|
+
[issues]: https://github.com/michaelherold/ksuid-ruby/issues
|
17
|
+
|
18
|
+
## Submitting an Issue
|
19
|
+
|
20
|
+
We use the [GitHub issue tracker][issues] to track bugs and features. Before submitting a bug report or feature request, check to make sure it hasn't already been submitted.
|
21
|
+
|
22
|
+
When submitting a bug report, please include a [Gist](https://gist.github.com) that includes a stack trace and any details that may be necessary to reproduce the bug, including your gem version, Ruby version, and operating system.
|
23
|
+
|
24
|
+
Ideally, a bug report should include a pull request with failing specs.
|
25
|
+
|
26
|
+
## Submitting a Pull Request
|
27
|
+
|
28
|
+
1. [Fork the repository].
|
29
|
+
2. [Create a topic branch].
|
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.
|
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.
|
34
|
+
7. Open `coverage/index.html`. If your changes are not completely covered by your tests, return to step 3.
|
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].
|
39
|
+
|
40
|
+
[Create a topic branch]: https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/
|
41
|
+
[Fork the repository]: http://learn.github.com/p/branching.html
|
42
|
+
[Submit a pull request]: https://help.github.com/articles/creating-a-pull-request/
|
43
|
+
|
44
|
+
## Tools to Help You Succeed
|
45
|
+
|
46
|
+
After checking out the repository, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
47
|
+
|
48
|
+
When writing code, you can use the helper application [Guard][guard] to automatically run tests and coverage tools whenever you modify and save a file. This helps to eliminate the tedium of running tests manually and reduces the chance that you will accidentally forget to run the tests. To use Guard, run `bundle exec guard`.
|
49
|
+
|
50
|
+
Before committing code, run `bundle exec rake` to check that the code conforms to the style guidelines of the project, that all of the tests are green (if you're writing a feature; if you're only submitting a failing test, then it does not have to pass!), and that the changes are sufficiently documented.
|
51
|
+
|
52
|
+
[guard]: http://guardgem.org
|
53
|
+
[rubygems]: https://rubygems.org
|
data/LICENSE.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright © 2017 Michael Herold
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# KSUID for Ruby
|
2
|
+
|
3
|
+
[][travis]
|
4
|
+
[][test-coverage]
|
5
|
+
[][maintainability]
|
6
|
+
[][inch]
|
7
|
+
|
8
|
+
[inch]: http://inch-ci.org/github/michaelherold/ksuid-ruby
|
9
|
+
[maintainability]: https://codeclimate.com/github/michaelherold/ksuid-ruby/maintainability
|
10
|
+
[test-coverage]: https://codeclimate.com/github/michaelherold/ksuid-ruby/test_coverage
|
11
|
+
[travis]: https://travis-ci.org/michaelherold/ksuid-ruby
|
12
|
+
|
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
|
+
|
15
|
+
---
|
16
|
+
|
17
|
+
# What is a KSUID?
|
18
|
+
|
19
|
+
KSUID is for K-Sortable Unique IDentifier. It's a way to generate globally unique IDs similar to RFC 4122 UUIDs, but contain a time component so they can be "roughly" sorted by time of creation. The remainder of the KSUID is randomly generated bytes.
|
20
|
+
|
21
|
+
# Why use KSUIDs?
|
22
|
+
|
23
|
+
Distributed systems often require unique IDs. There are numerous solutions out there for doing this, so why KSUID?
|
24
|
+
|
25
|
+
## 1. Sortable by Timestamp
|
26
|
+
|
27
|
+
Unlike the more common choice of UUIDv4, KSUIDs contain a timestamp component that allows them to be roughly sorted by generation time. This is obviously not a strong guarantee as it depends on wall clocks, but is still incredibly useful in practice.
|
28
|
+
|
29
|
+
## 2. No Coordination Required
|
30
|
+
|
31
|
+
[Snowflake IDs][1] and derivatives require coordination, which significantly increases the complexity of implementation and creates operations overhead. While RFC 4122 UUIDv1 does have a time component, there aren't enough bytes of randomness to provide strong protections against duplicate ID generation.
|
32
|
+
|
33
|
+
KSUIDs use 128-bits of pseudorandom data, which provides a 64-times larger number space than the 122-bits in the well-accepted RFC 4122 UUIDv4 standard. The additional timestamp component drives down the extremely rare chance of duplication to the point of near physical infeasibility, even assuming extreme clock skew (> 24-hours) that would cause other severe anomalies.
|
34
|
+
|
35
|
+
[1]: https://blog.twitter.com/2010/announcing-snowflake
|
36
|
+
|
37
|
+
## 3. Lexicographically Sortable, Portable Representations
|
38
|
+
|
39
|
+
The binary and string representations are lexicographically sortable, which allows them to be dropped into systems which do not natively support KSUIDs and retain their k-sortable characteristics.
|
40
|
+
|
41
|
+
The string representation is that it is base 62-encoded, so that they can "fit" anywhere alphanumeric strings are accepted.
|
42
|
+
|
43
|
+
# How do they work?
|
44
|
+
|
45
|
+
KSUIDs are 20-bytes: a 32-bit unsigned integer UTC timestamp and a 128-bit randomly generated payload. The timestamp uses big-endian encoding, to allow lexicographic sorting. The timestamp epoch is adjusted to March 5th, 2014, providing over 100 years of useful life starting at UNIX epoch + 14e8. The payload uses a cryptographically strong pseudorandom number generator.
|
46
|
+
|
47
|
+
The string representation is fixed at 27-characters encoded using a base 62 encoding that also sorts lexicographically.
|
48
|
+
|
49
|
+
---
|
50
|
+
|
51
|
+
## Installation
|
52
|
+
|
53
|
+
Add this line to your application's Gemfile:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
gem 'ksuid'
|
57
|
+
```
|
58
|
+
|
59
|
+
And then execute:
|
60
|
+
|
61
|
+
$ bundle
|
62
|
+
|
63
|
+
Or install it yourself as:
|
64
|
+
|
65
|
+
$ gem install ksuid
|
66
|
+
|
67
|
+
## Usage
|
68
|
+
|
69
|
+
To generate a KSUID for the present time, use:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
ksuid = KSUID.new
|
73
|
+
```
|
74
|
+
|
75
|
+
If you need to parse a KSUID from a string that you received, use the conversion method:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
ksuid = KSUID.from_base62(base62_string)
|
79
|
+
```
|
80
|
+
|
81
|
+
If you need to interpret a series of bytes that you received, use the conversion method:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
ksuid = KSUID.from_bytes(bytes)
|
85
|
+
```
|
86
|
+
|
87
|
+
The `KSUID.from_bytes` method can take either a byte string or a byte array.
|
88
|
+
|
89
|
+
If you need to generate a KSUID for a specific timestamp, use:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
ksuid = KSUID.new(time: time) # where time is a Time-like object
|
93
|
+
```
|
94
|
+
|
95
|
+
## Contributing
|
96
|
+
|
97
|
+
So you’re interested in contributing to KSUID? Check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to do that.
|
98
|
+
|
99
|
+
## Supported Ruby Versions
|
100
|
+
|
101
|
+
This library aims to support and is [tested against][travis] the following Ruby versions:
|
102
|
+
|
103
|
+
* Ruby 2.3
|
104
|
+
* Ruby 2.4
|
105
|
+
* JRuby 9.1
|
106
|
+
|
107
|
+
If something doesn't work on one of these versions, it's a bug.
|
108
|
+
|
109
|
+
This library may inadvertently work (or seem to work) on other Ruby versions, however support will only be provided for the versions listed above.
|
110
|
+
|
111
|
+
If you would like this library to support another Ruby version or implementation, you may volunteer to be a maintainer. Being a maintainer entails making sure all tests run and pass on that implementation. When something breaks on your implementation, you will be responsible for providing patches in a timely fashion. If critical issues for a particular implementation exist at the time of a major release, support for that Ruby version may be dropped.
|
112
|
+
|
113
|
+
## Versioning
|
114
|
+
|
115
|
+
This library aims to adhere to [Semantic Versioning 2.0.0][semver]. Violations of this scheme should be reported as bugs. Specifically, if a minor or patch version is released that breaks backward compatibility, that version should be immediately yanked and/or a new version should be immediately released that restores compatibility. Breaking changes to the public API will only be introduced with new major versions. As a result of this policy, you can (and should) specify a dependency on this gem using the [Pessimistic Version Constraint][pessimistic] with two digits of precision. For example:
|
116
|
+
|
117
|
+
spec.add_dependency "ksuid", "~> 0.1"
|
118
|
+
|
119
|
+
[pessimistic]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
|
120
|
+
[semver]: http://semver.org/spec/v2.0.0.html
|
121
|
+
|
122
|
+
## License
|
123
|
+
|
124
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/ksuid.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path(File.join('..', 'lib', 'ksuid', 'version'), __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'ksuid'
|
7
|
+
spec.version = KSUID::VERSION
|
8
|
+
spec.authors = ['Michael Herold']
|
9
|
+
spec.email = ['michael@michaeljherold.com']
|
10
|
+
|
11
|
+
spec.summary = 'Ruby implementation of the K-Sortable Unique IDentifier'
|
12
|
+
spec.description = spec.summary
|
13
|
+
spec.homepage = 'https://github.com/michaelherold/ksuid'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = %w[CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md]
|
17
|
+
spec.files += %w[ksuid.gemspec]
|
18
|
+
spec.files += Dir['lib/**/*.rb']
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
22
|
+
end
|
data/lib/ksuid.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'ksuid/type'
|
4
|
+
require_relative 'ksuid/version'
|
5
|
+
|
6
|
+
# The K-Sortable Unique IDentifier (KSUID)
|
7
|
+
#
|
8
|
+
# Distributed systems require unique identifiers to track events throughout
|
9
|
+
# their subsystems. Many algorithms for generating unique identifiers, like the
|
10
|
+
# {https://blog.twitter.com/2010/announcing-snowflake Snowflake ID} system,
|
11
|
+
# require coordination with a central authority. This is an unacceptable
|
12
|
+
# constraint in the face of systems that run on client devices, yet we still
|
13
|
+
# need to be able to generate event identifiers and roughly sort them for
|
14
|
+
# processing.
|
15
|
+
#
|
16
|
+
# The KSUID optimizes this problem into a roughly sortable identifier with
|
17
|
+
# a high possibility space to reduce the chance of collision. KSUID uses
|
18
|
+
# a 32-bit timestamp with second-level precision combined with 128 bytes of
|
19
|
+
# random data for the "payload". The timestamp is based on the Unix epoch, but
|
20
|
+
# with its base shifted forward from 1970-01-01 00:00:00 UTC to 2014-05-13
|
21
|
+
# 16:532:20 UTC. This is to extend the useful life of the ID format to over
|
22
|
+
# 100 years.
|
23
|
+
#
|
24
|
+
# Because KSUID timestamps use seconds as their unit of precision, they are
|
25
|
+
# unsuitable to tasks that require extreme levels of precision. If you need
|
26
|
+
# microsecond-level precision, a format like {https://github.com/alizain/ulid
|
27
|
+
# ULID} may be more suitable for your use case.
|
28
|
+
#
|
29
|
+
# KSUIDs are "roughly sorted". Practically, this means that for any given event
|
30
|
+
# stream, there may be some events that are ordered in a slightly different way
|
31
|
+
# than they actually happened. There are two reasons for this. Firstly, the
|
32
|
+
# format is precise to the second. This means that two events that are
|
33
|
+
# generated in the same second will be sorted together, but the KSUID with the
|
34
|
+
# smaller payload value will be sorted first. Secondly, the format is generated
|
35
|
+
# on the client device using its clock, so KSUID is susceptible to clock shift
|
36
|
+
# as well. The result of sorting the identifiers is that they will be sorted
|
37
|
+
# into groups of identifiers that happened in the same second according to
|
38
|
+
# their generating device.
|
39
|
+
#
|
40
|
+
# @example Generate a new KSUID
|
41
|
+
# KSUID.new
|
42
|
+
#
|
43
|
+
# @example Parse a KSUID string that you have received
|
44
|
+
# KSUID.from_base62('aWgEPTl1tmebfsQzFP4bxwgy80V')
|
45
|
+
#
|
46
|
+
# @example Parse a KSUID byte string that you have received
|
47
|
+
# KSUID.from_bytes(
|
48
|
+
# "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
|
49
|
+
# )
|
50
|
+
#
|
51
|
+
# @example Parse a KSUID byte array that you have received
|
52
|
+
# KSUID.from_bytes(
|
53
|
+
# [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
54
|
+
# 255, 255, 255, 255]
|
55
|
+
# )
|
56
|
+
module KSUID
|
57
|
+
# The shift in the Unix epoch time between the standard and the KSUID base
|
58
|
+
#
|
59
|
+
# @return [Integer] the number of seconds by which we shift the epoch
|
60
|
+
EPOCH_TIME = 1_400_000_000
|
61
|
+
|
62
|
+
# The number of bytes that are used to represent each part of a KSUID
|
63
|
+
#
|
64
|
+
# @return [Hash{Symbol => Integer}] the map of data type to number of bytes
|
65
|
+
BYTES = { payload: 16, timestamp: 4, total: 20 }.freeze
|
66
|
+
|
67
|
+
# The number of characters in a base 62-encoded KSUID
|
68
|
+
#
|
69
|
+
# @return [Integer]
|
70
|
+
STRING_LENGTH = 27
|
71
|
+
|
72
|
+
# The maximum KSUID as a base 62-encoded string.
|
73
|
+
#
|
74
|
+
# @return [String]
|
75
|
+
MAX_STRING_ENCODED = 'aWgEPTl1tmebfsQzFP4bxwgy80V'
|
76
|
+
|
77
|
+
# Converts a base 62-encoded string into a KSUID
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
#
|
81
|
+
# @example Parse a KSUID string into an object
|
82
|
+
# KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW')
|
83
|
+
#
|
84
|
+
# @param string [String] the base 62-encoded KSUID to convert into an object
|
85
|
+
# @return [KSUID::Type] the KSUID generated from the string
|
86
|
+
def self.from_base62(string)
|
87
|
+
string = string.rjust(STRING_LENGTH, Base62::CHARSET[0]) if string.length < STRING_LENGTH
|
88
|
+
int = Base62.decode(string)
|
89
|
+
bytes = Utils.int_to_bytes(int, 160)
|
90
|
+
|
91
|
+
from_bytes(bytes)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Converts a byte string or byte array into a KSUID
|
95
|
+
#
|
96
|
+
# @api public
|
97
|
+
#
|
98
|
+
# @example Parse a KSUID byte string into an object
|
99
|
+
# KSUID.from_bytes("\x06\x83\xF7\x89\x04\x9C\xC2\x15\xC0\x99\xD4+xM\xBE\x994\e\xD7\x9C")
|
100
|
+
#
|
101
|
+
# @param bytes [String|Array<Integer>] the byte string or array to convert into an object
|
102
|
+
# @return [KSUID::Type] the KSUID generated from the bytes
|
103
|
+
def self.from_bytes(bytes)
|
104
|
+
bytes = bytes.bytes if bytes.is_a?(String)
|
105
|
+
|
106
|
+
timestamp = Utils.int_from_bytes(bytes.first(BYTES[:timestamp]))
|
107
|
+
payload = Utils.byte_string_from_array(bytes.last(BYTES[:payload]))
|
108
|
+
|
109
|
+
KSUID::Type.new(payload: payload, time: Time.at(timestamp + EPOCH_TIME))
|
110
|
+
end
|
111
|
+
|
112
|
+
# Generates the maximum KSUID as a KSUID type
|
113
|
+
#
|
114
|
+
# @api semipublic
|
115
|
+
#
|
116
|
+
# @example Generate the maximum KSUID
|
117
|
+
# KSUID.max
|
118
|
+
#
|
119
|
+
# @return [KSUID::Type] the maximum KSUID in both timestamp and payload
|
120
|
+
def self.max
|
121
|
+
from_bytes([255] * BYTES[:total])
|
122
|
+
end
|
123
|
+
|
124
|
+
# Instantiates a new KSUID
|
125
|
+
#
|
126
|
+
# @api public
|
127
|
+
#
|
128
|
+
# @example Generate a new KSUID for the current second
|
129
|
+
# KSUID.new
|
130
|
+
#
|
131
|
+
# @example Generate a new KSUID for a given timestamp
|
132
|
+
# KSUID.new(time: Time.parse('2017-11-05 15:00:04 UTC'))
|
133
|
+
#
|
134
|
+
# @param payload [String|Array<Integer>|nil] the payload for the KSUID
|
135
|
+
# @param time [Time] the timestamp to use for the KSUID
|
136
|
+
# @return [KSUID::Type] the generated KSUID
|
137
|
+
def self.new(payload: nil, time: Time.now)
|
138
|
+
Type.new(payload: payload, time: time)
|
139
|
+
end
|
140
|
+
end
|
data/lib/ksuid/base62.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'utils'
|
4
|
+
|
5
|
+
module KSUID
|
6
|
+
# Converts between numbers and an alphanumeric encoding
|
7
|
+
#
|
8
|
+
# We store and report KSUIDs as base 62-encoded numbers to make them
|
9
|
+
# lexicographically sortable and compact to transmit. The base 62 alphabet
|
10
|
+
# consists of the Arabic numerals, followed by the English capital letters
|
11
|
+
# and the English lowercase letters.
|
12
|
+
module Base62
|
13
|
+
# The character set used to encode numbers into base 62
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
17
|
+
|
18
|
+
# The base (62) that this module encodes numbers into
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
BASE = CHARSET.size
|
22
|
+
|
23
|
+
# Decodes a base 62-encoded string into an integer
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
#
|
27
|
+
# @example Decode a string into a number
|
28
|
+
# KSUID::Base62.decode('0000000000000000000001LY7VK')
|
29
|
+
# #=> 1234567890
|
30
|
+
#
|
31
|
+
# @param ksuid [String] the base 62-encoded number
|
32
|
+
# @return [Integer] the decoded number as an integer
|
33
|
+
def self.decode(ksuid)
|
34
|
+
result = 0
|
35
|
+
|
36
|
+
ksuid.split('').each_with_index do |char, position|
|
37
|
+
unless (digit = CHARSET.index(char))
|
38
|
+
raise(ArgumentError, "#{ksuid} is not a base 62 number")
|
39
|
+
end
|
40
|
+
|
41
|
+
result += digit * BASE**(ksuid.length - (position + 1))
|
42
|
+
end
|
43
|
+
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
# Encodes a number (integer) as a base 62 string
|
48
|
+
#
|
49
|
+
# @api public
|
50
|
+
#
|
51
|
+
# @example Encode a number as a base 62 string
|
52
|
+
# KSUID::Base62.encode(1_234_567_890)
|
53
|
+
# #=> "0000000000000000000001LY7VK"
|
54
|
+
#
|
55
|
+
# @param number [Integer] the number to encode into base 62
|
56
|
+
# @return [String] the base 62-encoded number
|
57
|
+
def self.encode(number)
|
58
|
+
chars = encode_without_padding(number)
|
59
|
+
|
60
|
+
chars << padding if chars.empty?
|
61
|
+
chars.reverse.join('').rjust(STRING_LENGTH, padding)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Encodes a byte string or byte array into base 62
|
65
|
+
#
|
66
|
+
# @api semipublic
|
67
|
+
#
|
68
|
+
# @example Encode a maximal KSUID as a string
|
69
|
+
# KSUID::Base62.encode_bytes(
|
70
|
+
# [255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
71
|
+
# 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
|
72
|
+
# )
|
73
|
+
#
|
74
|
+
# @param bytes [String|Array<Integer>] the bytes to encode
|
75
|
+
# @return [String] the encoded bytes as a base 62 string
|
76
|
+
def self.encode_bytes(bytes)
|
77
|
+
encode(Utils.int_from_bytes(bytes))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Encodes a number as a string while disregarding the expected width
|
81
|
+
#
|
82
|
+
# @api private
|
83
|
+
#
|
84
|
+
# @param number [Integer] the number to encode
|
85
|
+
# @return [String] the resulting encoded string
|
86
|
+
def self.encode_without_padding(number)
|
87
|
+
[].tap do |chars|
|
88
|
+
loop do
|
89
|
+
break unless number.positive?
|
90
|
+
|
91
|
+
number, remainder = number.divmod(BASE)
|
92
|
+
chars << CHARSET[remainder]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
private_class_method :encode_without_padding
|
97
|
+
|
98
|
+
# The character used as padding in strings that are less than 27 characters
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
# @return [String]
|
102
|
+
def self.padding
|
103
|
+
CHARSET[0]
|
104
|
+
end
|
105
|
+
private_class_method :padding
|
106
|
+
end
|
107
|
+
end
|
data/lib/ksuid/type.rb
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require_relative 'base62'
|
5
|
+
require_relative 'utils'
|
6
|
+
|
7
|
+
module KSUID
|
8
|
+
# Encapsulates the data type for a KSUID
|
9
|
+
#
|
10
|
+
# This is the main class that you will interact with in this gem. You will
|
11
|
+
# not typically generate these directly, but this is the resulting data type
|
12
|
+
# for all of the main generation methods on the {KSUID} module.
|
13
|
+
#
|
14
|
+
# A KSUID type has two pieces of information contained within its
|
15
|
+
# byte-encoded data:
|
16
|
+
#
|
17
|
+
# 1. The timestamp associated with the KSUID (stored as the first 4 bytes)
|
18
|
+
# 2. The payload, or random data, for the KSUID (stored as the last 16 bytes)
|
19
|
+
#
|
20
|
+
# The type gives you access to several handles into these data.
|
21
|
+
class Type
|
22
|
+
include Comparable
|
23
|
+
|
24
|
+
# Instantiates a new KSUID type
|
25
|
+
#
|
26
|
+
# @api semipublic
|
27
|
+
#
|
28
|
+
# @example Generate a new KSUID for the current second
|
29
|
+
# KSUID::Type.new
|
30
|
+
#
|
31
|
+
# @example Generate a new KSUID for a given timestamp
|
32
|
+
# KSUID::Type.new(time: Time.parse('2017-11-05 15:00:04 UTC'))
|
33
|
+
#
|
34
|
+
# @param payload [String|Array<Integer>|nil] the payload for the KSUID
|
35
|
+
# @param time [Time] the timestamp to use for the KSUID
|
36
|
+
# @return [KSUID::Type] the generated KSUID
|
37
|
+
def initialize(payload: nil, time: Time.now)
|
38
|
+
payload ||= SecureRandom.random_bytes(BYTES[:payload])
|
39
|
+
byte_encoding = Utils.int_to_bytes(time.to_i - EPOCH_TIME)
|
40
|
+
|
41
|
+
@uid = byte_encoding.bytes + payload.bytes
|
42
|
+
end
|
43
|
+
|
44
|
+
# Implements the Comparable interface for sorting KSUIDs
|
45
|
+
#
|
46
|
+
# @api private
|
47
|
+
#
|
48
|
+
# @param other [KSUID::Type] the other object to compare against
|
49
|
+
# @return [Integer] -1 for less than other, 0 for equal to, 1 for greater than other
|
50
|
+
def <=>(other)
|
51
|
+
to_time <=> other.to_time
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks whether this KSUID is equal to another
|
55
|
+
#
|
56
|
+
# @api semipublic
|
57
|
+
#
|
58
|
+
# @example Checks whether two KSUIDs are equal
|
59
|
+
# KSUID.new == KSUID.new
|
60
|
+
#
|
61
|
+
# @param other [KSUID::Type] the other KSUID to check against
|
62
|
+
# @return [Boolean]
|
63
|
+
def ==(other)
|
64
|
+
other.to_s == to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
# The payload for the KSUID, as a hex-encoded string
|
68
|
+
#
|
69
|
+
# This is generally useful for comparing against the Go tool
|
70
|
+
#
|
71
|
+
# @api public
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW')
|
75
|
+
#
|
76
|
+
# ksuid.payload #=> "049CC215C099D42B784DBE99341BD79C"
|
77
|
+
#
|
78
|
+
# @return [String] a hex-encoded string
|
79
|
+
def payload
|
80
|
+
Utils.bytes_to_hex_string(uid.last(BYTES[:payload]))
|
81
|
+
end
|
82
|
+
|
83
|
+
# The KSUID as a hex-encoded string
|
84
|
+
#
|
85
|
+
# This is generally useful for comparing against the Go tool.
|
86
|
+
#
|
87
|
+
# @api public
|
88
|
+
#
|
89
|
+
# @example
|
90
|
+
# ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW')
|
91
|
+
#
|
92
|
+
# ksuid.raw #=> "0683F789049CC215C099D42B784DBE99341BD79C"
|
93
|
+
#
|
94
|
+
# @return [String] a hex-encoded string
|
95
|
+
def raw
|
96
|
+
Utils.bytes_to_hex_string(uid)
|
97
|
+
end
|
98
|
+
|
99
|
+
# The KSUID as a byte string
|
100
|
+
#
|
101
|
+
# @api public
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW')
|
105
|
+
#
|
106
|
+
# ksuid.to_bytes
|
107
|
+
#
|
108
|
+
# @return [String] a byte string
|
109
|
+
def to_bytes
|
110
|
+
Utils.byte_string_from_array(uid)
|
111
|
+
end
|
112
|
+
|
113
|
+
# The KSUID as a Unix timestamp
|
114
|
+
#
|
115
|
+
# @api public
|
116
|
+
#
|
117
|
+
# @example
|
118
|
+
# ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW')
|
119
|
+
#
|
120
|
+
# ksuid.to_i #=> 109311881
|
121
|
+
#
|
122
|
+
# @return [Integer] the Unix timestamp for the event (without the epoch shift)
|
123
|
+
def to_i
|
124
|
+
unix_time = Utils.int_from_bytes(uid.first(BYTES[:timestamp]))
|
125
|
+
|
126
|
+
unix_time
|
127
|
+
end
|
128
|
+
|
129
|
+
# The KSUID as a base 62-encoded string
|
130
|
+
#
|
131
|
+
# @api public
|
132
|
+
#
|
133
|
+
# @example
|
134
|
+
# ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW')
|
135
|
+
#
|
136
|
+
# ksuid.to_s #=> "0vdbMgWkU6slGpLVCqEFwkkZvuW"
|
137
|
+
#
|
138
|
+
# @return [String] the base 62-encoded string for the KSUID
|
139
|
+
def to_s
|
140
|
+
Base62.encode_bytes(uid)
|
141
|
+
end
|
142
|
+
|
143
|
+
# The time the KSUID was generated
|
144
|
+
#
|
145
|
+
# @api public
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW')
|
149
|
+
#
|
150
|
+
# ksuid.to_time.utc.to_s #=> "2017-10-29 21:18:01 UTC"
|
151
|
+
#
|
152
|
+
# @return [String] the base 62-encoded string for the KSUID
|
153
|
+
def to_time
|
154
|
+
Time.at(to_i + EPOCH_TIME)
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# The KSUID as a byte array
|
160
|
+
#
|
161
|
+
# @api private
|
162
|
+
#
|
163
|
+
# @return [Array<Integer>]
|
164
|
+
attr_reader :uid
|
165
|
+
end
|
166
|
+
end
|
data/lib/ksuid/utils.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KSUID
|
4
|
+
# Utility functions for converting between different encodings
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
module Utils
|
8
|
+
# Converts a byte string into a byte array
|
9
|
+
#
|
10
|
+
# @param bytes [String] a byte string
|
11
|
+
# @return [Array<Integer>] an array of bytes from the byte string
|
12
|
+
def self.byte_string_from_array(bytes)
|
13
|
+
bytes.pack('C*')
|
14
|
+
end
|
15
|
+
|
16
|
+
# Converts a byte string or byte array into a hex-encoded string
|
17
|
+
#
|
18
|
+
# @param bytes [String|Array<Integer>] the byte string or array
|
19
|
+
# @return [String] the byte string as a hex-encoded string
|
20
|
+
def self.bytes_to_hex_string(bytes)
|
21
|
+
bytes = bytes.bytes if bytes.is_a?(String)
|
22
|
+
|
23
|
+
byte_string_from_array(bytes)
|
24
|
+
.unpack('H*')
|
25
|
+
.first
|
26
|
+
.upcase
|
27
|
+
end
|
28
|
+
|
29
|
+
# Converts a byte string or byte array into an integer
|
30
|
+
#
|
31
|
+
# @param bytes [String|Array<Integer>] the byte string or array
|
32
|
+
# @return [Integer] the resulting integer
|
33
|
+
def self.int_from_bytes(bytes)
|
34
|
+
bytes = bytes.bytes if bytes.is_a?(String)
|
35
|
+
|
36
|
+
bytes
|
37
|
+
.map { |byte| byte.to_s(2).rjust(8, '0') }
|
38
|
+
.join('')
|
39
|
+
.to_i(2)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Converts an integer into a network-ordered (big endian) byte string
|
43
|
+
#
|
44
|
+
# @param int [Integer] the integer to convert
|
45
|
+
# @param bits [Integer] the expected number of bits for the result
|
46
|
+
# @return [String] the byte string
|
47
|
+
def self.int_to_bytes(int, bits = 32)
|
48
|
+
int
|
49
|
+
.to_s(2)
|
50
|
+
.rjust(bits, '0')
|
51
|
+
.split('')
|
52
|
+
.each_slice(8)
|
53
|
+
.map { |digits| digits.join.to_i(2) }
|
54
|
+
.pack("C#{bits / 8}")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ksuid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Herold
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
description: Ruby implementation of the K-Sortable Unique IDentifier
|
28
|
+
email:
|
29
|
+
- michael@michaeljherold.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- CHANGELOG.md
|
35
|
+
- CONTRIBUTING.md
|
36
|
+
- LICENSE.md
|
37
|
+
- README.md
|
38
|
+
- ksuid.gemspec
|
39
|
+
- lib/ksuid.rb
|
40
|
+
- lib/ksuid/base62.rb
|
41
|
+
- lib/ksuid/type.rb
|
42
|
+
- lib/ksuid/utils.rb
|
43
|
+
- lib/ksuid/version.rb
|
44
|
+
homepage: https://github.com/michaelherold/ksuid
|
45
|
+
licenses:
|
46
|
+
- MIT
|
47
|
+
metadata: {}
|
48
|
+
post_install_message:
|
49
|
+
rdoc_options: []
|
50
|
+
require_paths:
|
51
|
+
- lib
|
52
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
requirements: []
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 2.6.13
|
65
|
+
signing_key:
|
66
|
+
specification_version: 4
|
67
|
+
summary: Ruby implementation of the K-Sortable Unique IDentifier
|
68
|
+
test_files: []
|
69
|
+
has_rdoc:
|