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.
@@ -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
@@ -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
@@ -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
@@ -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.
@@ -0,0 +1,124 @@
1
+ # KSUID for Ruby
2
+
3
+ [![Build Status](https://travis-ci.org/michaelherold/ksuid-ruby.svg)][travis]
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/94b2a2d4082bff21c10f/test_coverage)][test-coverage]
5
+ [![Maintainability](https://api.codeclimate.com/v1/badges/94b2a2d4082bff21c10f/maintainability)][maintainability]
6
+ [![Inline docs](http://inch-ci.org/github/michaelherold/ksuid-ruby.svg?branch=master)][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).
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KSUID
4
+ # The version of the KSUID gem
5
+ #
6
+ # @return [String]
7
+ VERSION = '0.1.0'
8
+ 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: