ksuid 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: