├── .codeclimate.yml ├── .document ├── .github └── workflows │ ├── ci.yml │ └── linters.yml ├── .gitignore ├── .rubocop.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── README.md ├── activerecord-ksuid ├── .reek.yml ├── .rspec ├── .yardopts ├── .yardstick.yml ├── Appraisals ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── Rakefile ├── UPGRADING.md ├── activerecord-ksuid.gemspec ├── bin │ ├── console │ └── setup ├── checksums │ └── activerecord-ksuid-1.0.0.gem.sha512 ├── docker-compose.yml ├── gemfiles │ ├── rails_6.0.gemfile │ ├── rails_6.0.gemfile.lock │ ├── rails_6.1.gemfile │ ├── rails_6.1.gemfile.lock │ ├── rails_7.0.gemfile │ └── rails_7.0.gemfile.lock ├── lib │ ├── active_record │ │ ├── ksuid.rb │ │ └── ksuid │ │ │ ├── binary_type.rb │ │ │ ├── prefixed_type.rb │ │ │ ├── railtie.rb │ │ │ ├── table_definition.rb │ │ │ ├── type.rb │ │ │ └── version.rb │ └── activerecord-ksuid.rb └── spec │ ├── active_record │ └── ksuid │ │ └── railtie_spec.rb │ ├── doctest_helper.rb │ └── spec_helper.rb └── ksuid ├── .reek.yml ├── .rspec ├── .yardopts ├── .yardstick.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Guardfile ├── LICENSE.md ├── README.md ├── Rakefile ├── UPGRADING.md ├── benchmark ├── changes.rb ├── charset_inclusion.rb └── random_generators.rb ├── bin ├── console └── setup ├── checksums └── ksuid-1.0.0.gem.sha512 ├── ksuid.gemspec ├── lib ├── ksuid.rb └── ksuid │ ├── base62.rb │ ├── configuration.rb │ ├── prefixed.rb │ ├── type.rb │ ├── utils.rb │ └── version.rb └── spec ├── compatibility_spec.rb ├── doctest_helper.rb ├── ksuid ├── base62_spec.rb ├── configuration_spec.rb ├── prefixed_spec.rb ├── type_spec.rb └── utils_spec.rb ├── ksuid_spec.rb └── spec_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - "ruby" 7 | reek: 8 | enabled: true 9 | rubocop: 10 | enabled: true 11 | channel: rubocop-0-92 12 | 13 | ratings: 14 | paths: 15 | - "Gemfile.lock" 16 | - "**.rb" 17 | 18 | exclude_paths: 19 | - "spec/**/*.rb" 20 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | activerecord-ksuid/lib/**/*.rb 2 | ksuid/lib/**/*.rb 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | test-ksuid: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | ruby: 17 | - "2.7" 18 | - "3.0" 19 | - "3.1" 20 | - jruby-9.3 21 | name: Test ksuid on Ruby ${{ matrix.ruby }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby }} 27 | - uses: actions/cache@v3 28 | with: 29 | path: ksuid/vendor/bundle 30 | key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ hashFiles('Gemfile.lock') }} 31 | restore-keys: | 32 | ${{ runner.os }}-gems-${{ matrix.ruby }}- 33 | - name: Run test suite 34 | env: 35 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 36 | run: | 37 | cd ./ksuid 38 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 39 | chmod +x ./cc-test-reporter 40 | gem install bundler 41 | bundle config path vendor/bundle 42 | bundle check || bundle install --jobs 4 --retry 3 43 | ./cc-test-reporter before-build 44 | bundle exec rake spec 45 | ./cc-test-reporter after-build --exit-code $? 46 | 47 | test-activerecord-ksuid-mysql: 48 | runs-on: ubuntu-latest 49 | env: 50 | DB_HOST: 127.0.0.1 51 | DB_USERNAME: root 52 | DRIVER: mysql2 53 | services: 54 | mysql: 55 | image: mysql:latest 56 | env: 57 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 58 | MYSQL_DATABASE: activerecord-ksuid_test 59 | MYSQL_ROOT_PASSWORD: "" 60 | options: >- 61 | --health-cmd="mysqladmin ping" 62 | --health-interval=10s 63 | --health-timeout=5s 64 | --health-retries=3 65 | ports: 66 | - 3306:3306 67 | strategy: 68 | matrix: 69 | ruby: 70 | - "2.7" 71 | - "3.0" 72 | - "3.1" 73 | - jruby-9.3 74 | rails: 75 | - "6.0" 76 | - "6.1" 77 | - "7.0" 78 | exclude: 79 | - ruby: "3.0" 80 | rails: "6.0" 81 | - ruby: "3.1" 82 | rails: "6.0" 83 | - ruby: jruby-9.3 84 | rails: "7.0" 85 | name: Test activerecord-ksuid on Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}, and MySQL 86 | steps: 87 | - uses: actions/checkout@v3 88 | - uses: ruby/setup-ruby@v1 89 | with: 90 | ruby-version: ${{ matrix.ruby }} 91 | - uses: actions/cache@v3 92 | with: 93 | path: activerecord-ksuid/vendor/bundle 94 | key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('Gemfile.lock', 'activerecord-ksuid/gemfiles/*.gemfile.lock') }} 95 | restore-keys: | 96 | ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}- 97 | - name: Run test suite 98 | env: 99 | BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile 100 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 101 | run: | 102 | cd ./activerecord-ksuid 103 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 104 | chmod +x ./cc-test-reporter 105 | gem install bundler 106 | bundle config path vendor/bundle 107 | bundle check || bundle install --jobs 4 --retry 3 108 | ./cc-test-reporter before-build 109 | bundle exec rspec 110 | ./cc-test-reporter after-build --exit-code $? 111 | 112 | test-activerecord-ksuid-postgresql: 113 | runs-on: ubuntu-latest 114 | env: 115 | DB_HOST: 127.0.0.1 116 | DB_USERNAME: postgres 117 | DRIVER: postgresql 118 | services: 119 | postgres: 120 | image: postgres:latest 121 | env: 122 | POSTGRES_DB: activerecord-ksuid_test 123 | POSTGRES_HOST_AUTH_METHOD: trust 124 | options: >- 125 | --health-cmd pg_isready 126 | --health-interval 10s 127 | --health-timeout 5s 128 | --health-retries 5 129 | ports: 130 | - 5432:5432 131 | strategy: 132 | matrix: 133 | ruby: 134 | - "2.7" 135 | - "3.0" 136 | - "3.1" 137 | - jruby-9.3 138 | rails: 139 | - "6.0" 140 | - "6.1" 141 | - "7.0" 142 | exclude: 143 | - ruby: "3.0" 144 | rails: "6.0" 145 | - ruby: "3.1" 146 | rails: "6.0" 147 | - ruby: jruby-9.3 148 | rails: "7.0" 149 | name: Test activerecord-ksuid on Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}, and PostgreSQL 150 | steps: 151 | - uses: actions/checkout@v3 152 | - uses: ruby/setup-ruby@v1 153 | with: 154 | ruby-version: ${{ matrix.ruby }} 155 | - uses: actions/cache@v3 156 | with: 157 | path: activerecord-ksuid/vendor/bundle 158 | key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('Gemfile.lock', 'activerecord-ksuid/gemfiles/*.gemfile.lock') }} 159 | restore-keys: | 160 | ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}- 161 | - name: Run test suite 162 | env: 163 | BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile 164 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 165 | run: | 166 | cd ./activerecord-ksuid 167 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 168 | chmod +x ./cc-test-reporter 169 | gem install bundler 170 | bundle config path vendor/bundle 171 | bundle check || bundle install --jobs 4 --retry 3 172 | ./cc-test-reporter before-build 173 | bundle exec rspec 174 | ./cc-test-reporter after-build --exit-code $? 175 | 176 | test-activerecord-ksuid-sqlite: 177 | runs-on: ubuntu-latest 178 | env: 179 | DATABASE: ":memory:" 180 | DRIVER: sqlite3 181 | strategy: 182 | matrix: 183 | ruby: 184 | - "2.7" 185 | - "3.0" 186 | - "3.1" 187 | - jruby-9.3 188 | rails: 189 | - "6.0" 190 | - "6.1" 191 | - "7.0" 192 | exclude: 193 | - ruby: "3.0" 194 | rails: "6.0" 195 | - ruby: "3.1" 196 | rails: "6.0" 197 | - ruby: jruby-9.3 198 | rails: "7.0" 199 | name: Test activerecord-ksuid on Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }}, and SQLite 200 | steps: 201 | - uses: actions/checkout@v3 202 | - uses: ruby/setup-ruby@v1 203 | with: 204 | ruby-version: ${{ matrix.ruby }} 205 | - uses: actions/cache@v3 206 | with: 207 | path: activerecord-ksuid/vendor/bundle 208 | key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('Gemfile.lock', 'activerecord-ksuid/gemfiles/*.gemfile.lock') }} 209 | restore-keys: | 210 | ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}- 211 | - name: Run test suite 212 | env: 213 | BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile 214 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 215 | run: | 216 | cd ./activerecord-ksuid 217 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 218 | chmod +x ./cc-test-reporter 219 | gem install bundler 220 | bundle config path vendor/bundle 221 | bundle check || bundle install --jobs 4 --retry 3 222 | ./cc-test-reporter before-build 223 | bundle exec rspec 224 | ./cc-test-reporter after-build --exit-code $? 225 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | inch: 13 | runs-on: ubuntu-latest 14 | name: Inch 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: 2.7 20 | - uses: actions/cache@v3 21 | with: 22 | path: vendor/bundle 23 | key: ${{ runner.os }}-linting-${{ hashFiles('Gemfile.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-linting- 26 | - run: | 27 | gem install bundler 28 | bundle config path vendor/bundle 29 | bundle install --jobs 4 --retry 3 --with="linting" 30 | - name: Lint 31 | run: | 32 | bundle exec inch 33 | rubocop: 34 | runs-on: ubuntu-latest 35 | name: Rubocop 36 | steps: 37 | - uses: actions/checkout@v3 38 | - uses: ruby/setup-ruby@v1 39 | with: 40 | ruby-version: 2.7 41 | - uses: actions/cache@v3 42 | with: 43 | path: vendor/bundle 44 | key: ${{ runner.os }}-linting-${{ hashFiles('Gemfile.lock') }} 45 | restore-keys: | 46 | ${{ runner.os }}-linting- 47 | - run: | 48 | gem install bundler 49 | bundle config path vendor/bundle 50 | bundle install --jobs 4 --retry 3 --with="linting" 51 | - name: Lint 52 | run: | 53 | bundle exec rubocop 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.bundle/ 2 | **/.yardoc 3 | **/_yardoc/ 4 | **/coverage/ 5 | **/doc/ 6 | **/pkg/ 7 | **/spec/examples.txt 8 | **/spec/reports/ 9 | **/tmp/ 10 | *.jsonld 11 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rake 3 | - rubocop-rspec 4 | 5 | AllCops: 6 | Include: 7 | - "Gemfile" 8 | - "Guardfile" 9 | - "Rakefile" 10 | - "**/*.gemspec" 11 | - "**/lib/**/*.rb" 12 | - "**/spec/**/*.rb" 13 | Exclude: 14 | - "*.gemfile" 15 | - "vendor/bundle/**/*" 16 | - "tmp/**/*" 17 | NewCops: enable 18 | TargetRubyVersion: 2.7 19 | 20 | Gemspec/RequiredRubyVersion: 21 | Enabled: false 22 | 23 | Layout/LineLength: 24 | Max: 100 25 | 26 | Metrics/AbcSize: 27 | Exclude: 28 | - "Rakefile" 29 | - "**/spec/**/*_spec.rb" 30 | 31 | Metrics/BlockLength: 32 | Exclude: 33 | - "**/Rakefile" 34 | - "**/spec/**/*_spec.rb" 35 | 36 | Metrics/MethodLength: 37 | Exclude: 38 | - "Rakefile" 39 | - "**/spec/**/*_spec.rb" 40 | 41 | Naming/FileName: 42 | Exclude: 43 | - "**/Guardfile" 44 | - "**/Rakefile" 45 | - "activerecord-ksuid/lib/activerecord-ksuid.rb" 46 | 47 | RSpec/DescribeClass: 48 | IgnoredMetadata: 49 | type: 50 | - compatibility 51 | - integration 52 | 53 | RSpec/ExampleLength: 54 | Enabled: false 55 | 56 | # Disabled because it's invalid syntax on older Rubies 57 | # and we don't want to break compatibility before 1.0.0 58 | Style/SlicingWithRange: 59 | Enabled: false 60 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Citizen Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of KSUID for Ruby is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in KSUID for Ruby to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open Source Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people's personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone's consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Weapons Policy 47 | 48 | No weapons will be allowed at KSUID for Ruby events, community spaces, or in other spaces covered by the scope of this Code of Conduct. Weapons include but are not limited to guns, explosives (including fireworks), and large knives such as those used for hunting or display, as well as any other item used for the purpose of causing injury or harm to others. Anyone seen in possession of one of these items will be asked to leave immediately, and will only be allowed to return without the weapon. Community members are further expected to comply with all state and local laws on this matter. 49 | 50 | ## 6. Consequences of Unacceptable Behavior 51 | 52 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 53 | 54 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 55 | 56 | If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 57 | 58 | ## 7. Reporting Guidelines 59 | 60 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify us as soon as possible. 61 | 62 | Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. 63 | 64 | ## 8. Addressing Grievances 65 | 66 | If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify the maintainers with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. 67 | 68 | ## 9. Scope 69 | 70 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues--online and in-person--as well as in all one-on-one communications pertaining to community business. 71 | 72 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 73 | 74 | ## 10. Contact info 75 | 76 | This is not fully outlined yet and will not be until there is a community of more than one person involved in the project. 77 | 78 | ## 11. License and attribution 79 | 80 | The Citizen Code of Conduct is distributed by [Stumptown Syndicate](http://stumptownsyndicate.org) under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 81 | 82 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 83 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem 'ksuid', path: File.expand_path('./ksuid', __dir__) 8 | 9 | group :development do 10 | gem 'benchmark-ips' 11 | gem 'guard-bundler' 12 | gem 'guard-inch' 13 | gem 'guard-rspec' 14 | gem 'guard-rubocop' 15 | gem 'guard-yard' 16 | gem 'yard', '~> 0.9' 17 | gem 'yardstick' 18 | 19 | group :test do 20 | gem 'appraisal' 21 | gem 'pry' 22 | gem 'rake' 23 | gem 'rspec', '~> 3.6' 24 | gem 'simplecov', '< 0.18', require: false 25 | 26 | group :linting do 27 | gem 'yard-doctest' 28 | end 29 | end 30 | 31 | group :linting do 32 | gem 'inch' 33 | gem 'rubocop', '1.35.0' 34 | gem 'rubocop-rake' 35 | gem 'rubocop-rspec' 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ksuid 3 | specs: 4 | ksuid (1.0.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | appraisal (2.4.1) 10 | bundler 11 | rake 12 | thor (>= 0.14.0) 13 | ast (2.4.2) 14 | benchmark-ips (2.10.0) 15 | coderay (1.1.3) 16 | diff-lcs (1.5.0) 17 | docile (1.4.0) 18 | ffi (1.15.5) 19 | ffi (1.15.5-java) 20 | formatador (1.1.0) 21 | guard (2.18.0) 22 | formatador (>= 0.2.4) 23 | listen (>= 2.7, < 4.0) 24 | lumberjack (>= 1.0.12, < 2.0) 25 | nenv (~> 0.1) 26 | notiffany (~> 0.0) 27 | pry (>= 0.13.0) 28 | shellany (~> 0.0) 29 | thor (>= 0.18.1) 30 | guard-bundler (3.0.0) 31 | bundler (>= 2.1, < 3) 32 | guard (~> 2.2) 33 | guard-compat (~> 1.1) 34 | guard-compat (1.2.1) 35 | guard-inch (0.2.0) 36 | guard (~> 2) 37 | inch (~> 0) 38 | guard-rspec (4.7.3) 39 | guard (~> 2.1) 40 | guard-compat (~> 1.1) 41 | rspec (>= 2.99.0, < 4.0) 42 | guard-rubocop (1.5.0) 43 | guard (~> 2.0) 44 | rubocop (< 2.0) 45 | guard-yard (2.2.1) 46 | guard (>= 1.1.0) 47 | yard (>= 0.7.0) 48 | inch (0.8.0) 49 | pry 50 | sparkr (>= 0.2.0) 51 | term-ansicolor 52 | yard (~> 0.9.12) 53 | json (2.6.2) 54 | json (2.6.2-java) 55 | listen (3.7.1) 56 | rb-fsevent (~> 0.10, >= 0.10.3) 57 | rb-inotify (~> 0.9, >= 0.9.10) 58 | lumberjack (1.2.8) 59 | method_source (1.0.0) 60 | minitest (5.16.3) 61 | nenv (0.3.0) 62 | notiffany (0.1.3) 63 | nenv (~> 0.1) 64 | shellany (~> 0.0) 65 | parallel (1.22.1) 66 | parser (3.1.2.1) 67 | ast (~> 2.4.1) 68 | pry (0.14.1) 69 | coderay (~> 1.1) 70 | method_source (~> 1.0) 71 | pry (0.14.1-java) 72 | coderay (~> 1.1) 73 | method_source (~> 1.0) 74 | spoon (~> 0.0) 75 | rainbow (3.1.1) 76 | rake (13.0.6) 77 | rb-fsevent (0.11.2) 78 | rb-inotify (0.10.1) 79 | ffi (~> 1.0) 80 | regexp_parser (2.6.0) 81 | rexml (3.2.8) 82 | strscan (>= 3.0.9) 83 | rspec (3.11.0) 84 | rspec-core (~> 3.11.0) 85 | rspec-expectations (~> 3.11.0) 86 | rspec-mocks (~> 3.11.0) 87 | rspec-core (3.11.0) 88 | rspec-support (~> 3.11.0) 89 | rspec-expectations (3.11.1) 90 | diff-lcs (>= 1.2.0, < 2.0) 91 | rspec-support (~> 3.11.0) 92 | rspec-mocks (3.11.1) 93 | diff-lcs (>= 1.2.0, < 2.0) 94 | rspec-support (~> 3.11.0) 95 | rspec-support (3.11.1) 96 | rubocop (1.35.0) 97 | json (~> 2.3) 98 | parallel (~> 1.10) 99 | parser (>= 3.1.2.1) 100 | rainbow (>= 2.2.2, < 4.0) 101 | regexp_parser (>= 1.8, < 3.0) 102 | rexml (>= 3.2.5, < 4.0) 103 | rubocop-ast (>= 1.20.1, < 2.0) 104 | ruby-progressbar (~> 1.7) 105 | unicode-display_width (>= 1.4.0, < 3.0) 106 | rubocop-ast (1.21.0) 107 | parser (>= 3.1.1.0) 108 | rubocop-rake (0.6.0) 109 | rubocop (~> 1.0) 110 | rubocop-rspec (2.13.2) 111 | rubocop (~> 1.33) 112 | ruby-progressbar (1.11.0) 113 | shellany (0.0.1) 114 | simplecov (0.17.1) 115 | docile (~> 1.1) 116 | json (>= 1.8, < 3) 117 | simplecov-html (~> 0.10.0) 118 | simplecov-html (0.10.2) 119 | sparkr (0.4.1) 120 | spoon (0.0.6) 121 | ffi 122 | strscan (3.1.0) 123 | strscan (3.1.0-java) 124 | sync (0.5.0) 125 | term-ansicolor (1.7.1) 126 | tins (~> 1.0) 127 | thor (1.2.1) 128 | tins (1.31.1) 129 | sync 130 | unicode-display_width (2.3.0) 131 | yard (0.9.36) 132 | yard-doctest (0.1.17) 133 | minitest 134 | yard 135 | yardstick (0.9.9) 136 | yard (~> 0.8, >= 0.8.7.2) 137 | 138 | PLATFORMS 139 | ruby 140 | universal-java-11 141 | 142 | DEPENDENCIES 143 | appraisal 144 | benchmark-ips 145 | guard-bundler 146 | guard-inch 147 | guard-rspec 148 | guard-rubocop 149 | guard-yard 150 | inch 151 | ksuid! 152 | pry 153 | rake 154 | rspec (~> 3.6) 155 | rubocop (= 1.35.0) 156 | rubocop-rake 157 | rubocop-rspec 158 | simplecov (< 0.18) 159 | yard (~> 0.9) 160 | yard-doctest 161 | yardstick 162 | 163 | BUNDLED WITH 164 | 2.3.23 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KSUID for Ruby 2 | 3 | This is the repository that holds all KSUID for Ruby gems. What is a [KSUID]((https://github.com/segmentio/ksuid)), you ask? The original readme for the Go library does a great job of explaining what they are and how you can use them, so we excerpt it here. 4 | 5 | --- 6 | 7 | ## What is a KSUID? 8 | 9 | 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. 10 | 11 | ## Why use KSUIDs? 12 | 13 | Distributed systems often require unique IDs. There are numerous solutions out there for doing this, so why KSUID? 14 | 15 | ### 1. Sortable by Timestamp 16 | 17 | 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. 18 | 19 | ### 2. No Coordination Required 20 | 21 | [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. 22 | 23 | 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. 24 | 25 | [1]: https://blog.twitter.com/2010/announcing-snowflake 26 | 27 | ### 3. Lexicographically Sortable, Portable Representations 28 | 29 | 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. 30 | 31 | The string representation is that it is base 62-encoded, so that they can "fit" anywhere alphanumeric strings are accepted. 32 | 33 | ## How do they work? 34 | 35 | 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. 36 | 37 | The string representation is fixed at 27-characters encoded using a base 62 encoding that also sorts lexicographically. 38 | 39 | --- 40 | 41 | ## Contents of this repository 42 | 43 | Currently, there are two gems available: 44 | 45 | 1. [KSUID for Ruby](ksuid/README.md) provides the main data type and handling for KSUIDs. If you want to use them outside of a database, this will be all you need. 46 | 2. [KSUID for ActiveRecord](activerecord-ksuid/README.md) handles integrating with ActiveRecord. If you're using ActiveRecord and want to serialize KSUID columns, you will want this gem. 47 | -------------------------------------------------------------------------------- /activerecord-ksuid/.reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | detectors: 3 | BooleanParameter: 4 | exclude: 5 | - "ActiveRecord::KSUID#self.[]" 6 | 7 | ControlParameter: 8 | exclude: 9 | - "ActiveRecord::KSUID#self.[]" 10 | - "ActiveRecord::KSUID#self.define_attribute" 11 | 12 | UtilityFunction: 13 | exclude: 14 | - "ActiveRecord::KSUID::BinaryType" 15 | - "ActiveRecord::KSUID::Type" 16 | 17 | exclude_paths: 18 | - benchmark/ 19 | 20 | # vim: ft=yaml 21 | -------------------------------------------------------------------------------- /activerecord-ksuid/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /activerecord-ksuid/.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --protected 3 | --markup markdown 4 | --plugin yard-doctest 5 | - 6 | CHANGELOG.md 7 | CONTRIBUTING.md 8 | LICENSE.md 9 | README.md 10 | -------------------------------------------------------------------------------- /activerecord-ksuid/.yardstick.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 100 3 | -------------------------------------------------------------------------------- /activerecord-ksuid/Appraisals: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | appraise 'rails-6.0' do 4 | gemspec 5 | 6 | gem 'ksuid', path: '../ksuid' 7 | gem 'rails', '~> 6.0.0' 8 | 9 | platforms :jruby do 10 | gem 'activerecord-jdbcmysql-adapter', '~> 60' 11 | gem 'activerecord-jdbcpostgresql-adapter', '~> 60' 12 | gem 'activerecord-jdbcsqlite3-adapter', '~> 60' 13 | end 14 | 15 | platforms :mri, :mingw, :x64_mingw do 16 | gem 'mysql2', '>= 0.4.4' 17 | gem 'pg', '>= 0.18', '< 2.0' 18 | gem 'sqlite3', '~> 1.4' 19 | end 20 | end 21 | 22 | appraise 'rails-6.1' do 23 | gemspec 24 | 25 | gem 'ksuid', path: '../ksuid' 26 | gem 'rails', '~> 6.1.0' 27 | 28 | platforms :jruby do 29 | gem 'activerecord-jdbcmysql-adapter', '~> 61' 30 | gem 'activerecord-jdbcpostgresql-adapter', '~> 61' 31 | gem 'activerecord-jdbcsqlite3-adapter', '~> 61' 32 | end 33 | 34 | platforms :mri, :mingw, :x64_mingw do 35 | gem 'mysql2', '~> 0.5' 36 | gem 'pg', '~> 1.1' 37 | gem 'sqlite3', '~> 1.4' 38 | end 39 | end 40 | 41 | unless RUBY_ENGINE == 'jruby' 42 | appraise 'rails-7.0' do 43 | gemspec 44 | 45 | gem 'ksuid', path: '../ksuid' 46 | gem 'rails', '~> 7.0.0' 47 | 48 | platforms :mri, :mingw, :x64_mingw do 49 | gem 'mysql2', '~> 0.5' 50 | gem 'pg', '~> 1.1' 51 | gem 'sqlite3', '~> 1.4' 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /activerecord-ksuid/CHANGELOG.md: -------------------------------------------------------------------------------- 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 | ## [1.0.0](https://github.com/michaelherold/ksuid/compare/v0.5.0...v1.0.0) - 2023-02-25 8 | 9 | ### Added 10 | 11 | - Extracted the ActiveRecord behavior from [`ksuid-v0.5.0`](https://github.com/michaelherold/ksuid-ruby/tree/v0.5.0) into its own gem to slim down the gem and remove unnecessary functionality for people who only want the KSUID functionality. 12 | - Added the ability to disable the automatic generation of KSUIDs for fields by passing `auto_gen: false` to the module builder. This is helpful for foreign key fields, where an invalid value can raise errors, or for cases where you don't want to set the value until a later time. 13 | 14 | ### Fixed 15 | 16 | - Binary KSUIDs on PostgreSQL now correctly deserialize without any extra configuration. 17 | -------------------------------------------------------------------------------- /activerecord-ksuid/CONTRIBUTING.md: -------------------------------------------------------------------------------- 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 `appraisal rake spec`. If your specs pass, return to step 3. 32 | 5. Implement your feature or bug fix. 33 | 6. Run `appraisal rake`. If your specs or any of the linters fail, return to step 5. 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. Commit and push your changes. 37 | 10. [Submit a pull request]. 38 | 39 | [Create a topic branch]: https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/ 40 | [Fork the repository]: http://learn.github.com/p/branching.html 41 | [Submit a pull request]: https://help.github.com/articles/creating-a-pull-request/ 42 | 43 | ## Tools to Help You Succeed 44 | 45 | You will need a working copy of MySQL and PostgreSQL to run tests against them. At the root level of the gem, there is a `docker-compose.yml` file that sets up both of these services for you using either [Podman Compose](https://github.com/containers/podman-compose) or [Docker Compose](https://docs.docker.com/compose/). Both of these services will bind their default ports, so keep that in mind if you already have either database running on that port. Assuming you picked Podman, run: 46 | 47 | podman-compose up 48 | 49 | in the `activerecord-ksuid` directory to start the services. 50 | 51 | After checking out the repository, run `bin/setup` to install dependencies. Then, run `appraisal rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 52 | 53 | Before committing code, run `appraisal 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. 54 | 55 | [rubygems]: https://rubygems.org 56 | -------------------------------------------------------------------------------- /activerecord-ksuid/LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © 2017-2022 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. 21 | -------------------------------------------------------------------------------- /activerecord-ksuid/README.md: -------------------------------------------------------------------------------- 1 | # KSUID for ActiveRecord 2 | 3 | [![Build Status](https://github.com/michaelherold/ksuid-ruby/workflows/CI/badge.svg)][actions] 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=main)][inch] 7 | 8 | [actions]: https://github.com/michaelherold/ksuid-ruby/actions 9 | [inch]: http://inch-ci.org/github/michaelherold/ksuid-ruby 10 | [maintainability]: https://codeclimate.com/github/michaelherold/ksuid-ruby/maintainability 11 | [test-coverage]: https://codeclimate.com/github/michaelherold/ksuid-ruby/test_coverage 12 | 13 | activerecord-ksuid is a Ruby library that enables the use of [KSUIDs](https://github.com/segmentio/ksuid) within ActiveRecord. 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 'activerecord-ksuid', require: 'active_record/ksuid/railtie' 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 | Whether you are using ActiveRecord inside an existing project or in a new project, usage is simple. Additionally, you can use it with or without Rails. 70 | 71 | #### Adding to an existing model 72 | 73 | Within a Rails project, it is easy to get started using KSUIDs within your models. You can use the `ksuid` column type in a Rails migration to add a column to an existing model: 74 | 75 | rails generate migration add_ksuid_to_events unique_id:ksuid 76 | 77 | This will generate a migration like the following: 78 | 79 | ```ruby 80 | class AddKsuidToEvents < ActiveRecord::Migration[5.2] 81 | def change 82 | add_column :events, :unique_id, :ksuid 83 | end 84 | end 85 | ``` 86 | 87 | Then, to add proper handling to the field, you will want to mix a module into the model: 88 | 89 | ```ruby 90 | class Event < ApplicationRecord 91 | include ActiveRecord::KSUID[:unique_id] 92 | end 93 | ``` 94 | 95 | #### Creating a new model 96 | 97 | To create a new model with a `ksuid` field that serializes as a KSUID string, use the `ksuid` column type. Using the Rails generators, this looks like: 98 | 99 | rails generate model Event my_field_name:ksuid 100 | 101 | If you would like to add a KSUID to an existing model, you can do so with the following: 102 | 103 | ```ruby 104 | class AddKsuidToEvents < ActiveRecord::Migration[5.2] 105 | change_table :events do |table| 106 | table.ksuid :my_field_name 107 | end 108 | end 109 | ``` 110 | 111 | Once you have generated the table that you will use for your model, you will need to include a module into the model class, as follows: 112 | 113 | ```ruby 114 | class Event < ApplicationRecord 115 | include ActiveRecord::KSUID[:my_field_name] 116 | end 117 | ``` 118 | 119 | ##### With a KSUID primary key 120 | 121 | You can also use a KSUID as the primary key on a table, much like you can use a UUID in vanilla Rails. To do so requires a little more finagling than you can manage through the generators. When hand-writing the migration, it will look like this: 122 | 123 | ```ruby 124 | class CreateEvents < ActiveRecord::Migration[5.2] 125 | create_table :events, id: false do |table| 126 | table.ksuid :id, primary_key: true 127 | end 128 | end 129 | ``` 130 | 131 | You will need to mix in the module into your model as well: 132 | 133 | ```ruby 134 | class Event < ApplicationRecord 135 | include ActiveRecord::KSUID[:id] 136 | end 137 | ``` 138 | 139 | ##### Without generating a default value 140 | 141 | In some cases, such as foreign keys, you do not want to generate a default value for the field and, instead, want to set the value manually. When this is true, you can disable the default generation behavior by passing `auto_gen: false` to the module builder. 142 | 143 | ```ruby 144 | class Event < ApplicationRecord 145 | include ActiveRecord::KSUID[:correlation_id, auto_gen: false] 146 | 147 | belongs_to :correlation, class_name: Event 148 | end 149 | ``` 150 | 151 | You can also use the [Attributes API](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html) directly: 152 | 153 | ```ruby 154 | class Event < ApplicationRecord 155 | attribute :correlation_id, :ksuid 156 | 157 | belongs_to :correlation, class_name: Event 158 | end 159 | ``` 160 | 161 | #### Outside of Rails 162 | 163 | Outside of Rails, you cannot rely on the Railtie to load the appropriate files for you automatically. Toward the start of your application's boot process, you will want to require the following: 164 | 165 | ```ruby 166 | require 'ksuid' 167 | require 'active_record/ksuid' 168 | 169 | # If you will be using the ksuid column type in a migration 170 | require 'active_record/ksuid/table_definition' 171 | ``` 172 | 173 | Once you have required the file(s) that you need, everything else will work as it does above. 174 | 175 | #### Binary vs. String KSUIDs 176 | 177 | These examples all store your identifier as a string-based KSUID. If you would like to use binary KSUIDs instead, use the `ksuid_binary` column type. Unless you need to be super-efficient with your database, we recommend using string-based KSUIDs because it makes looking at the data while in the database a little easier to understand. 178 | 179 | When you include the KSUID module into your model, you will want to pass the `:binary` option as well: 180 | 181 | ```ruby 182 | class Event < ApplicationRecord 183 | include ActiveRecord::KSUID[:my_field_name, binary: true] 184 | end 185 | ``` 186 | 187 | #### Using a prefix on your KSUID field 188 | 189 | For prefixed KSUIDs in ActiveRecord, you must pass the intended prefix during table definition so that the field is of appropriate size. 190 | 191 | ```ruby 192 | class CreateEvents < ActiveRecord::Migration[5.2] 193 | create_table :events do |table| 194 | table.ksuid :ksuid, prefix: 'evt_' 195 | end 196 | end 197 | ``` 198 | 199 | You also must pass it in the module builder that you include in your model: 200 | 201 | ```ruby 202 | class Event < ApplicationRecord 203 | include ActiveRecord::KSUID[:ksuid, prefix: 'evt_'] 204 | end 205 | ``` 206 | 207 | You cannot use a prefix with a binary-encoded KSUID. 208 | 209 | #### Use the KSUID as your `created_at` timestamp 210 | 211 | Since KSUIDs include a timestamp as well, you can infer the `#created_at` timestamp from the KSUID. The module builder enables that option automatically with the `:created_at` option, like so: 212 | 213 | ```ruby 214 | class Event < ApplicationRecord 215 | include ActiveRecord::KSUID[:my_field_name, created_at: true] 216 | end 217 | ``` 218 | 219 | This allows you to be efficient in your database design if that is a constraint you need to satisfy. 220 | 221 | ## Contributing 222 | 223 | So you’re interested in contributing to KSUID for ActiveRecord? Check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to do that. 224 | 225 | ## Supported Ruby Versions 226 | 227 | This library aims to support and is [tested against][actions] the following Ruby versions: 228 | 229 | * Ruby 2.7 230 | * Ruby 3.0 231 | * Ruby 3.1 232 | * JRuby 9.3 233 | 234 | If something doesn't work on one of these versions, it's a bug. 235 | 236 | This library may inadvertently work (or seem to work) on other Ruby versions, however support will only be provided for the versions listed above. 237 | 238 | 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. 239 | 240 | ## Versioning 241 | 242 | 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: 243 | 244 | spec.add_dependency "activerecord-ksuid", "~> 1.0" 245 | 246 | [pessimistic]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint 247 | [semver]: http://semver.org/spec/v2.0.0.html 248 | 249 | ## License 250 | 251 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 252 | -------------------------------------------------------------------------------- /activerecord-ksuid/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | 5 | # Defines a Rake task if the optional dependency is installed 6 | # 7 | # @return [nil] 8 | def with_optional_dependency 9 | yield if block_given? 10 | rescue LoadError # rubocop:disable Lint/SuppressedException 11 | end 12 | 13 | default = %w[spec:sqlite] 14 | 15 | namespace :db do 16 | desc 'Reset all databases' 17 | task reset: %i[mysql:reset postgresql:reset] 18 | 19 | namespace :mysql do 20 | desc 'Reset MySQL database' 21 | task reset: %i[drop create] 22 | 23 | desc 'Create MySQL database' 24 | task :create do 25 | sh %(mysql -u root -e 'CREATE DATABASE `activerecord-ksuid_test`;') 26 | end 27 | 28 | desc 'Drops MySQL database' 29 | task :drop do 30 | sh %(mysql -u root -e 'DROP DATABASE IF EXISTS `activerecord-ksuid_test`;') 31 | end 32 | end 33 | 34 | namespace :postgresql do 35 | desc 'Reset PostgreSQL database' 36 | task reset: %i[drop create] 37 | 38 | desc 'Create PostgreSQL database' 39 | task :create do 40 | sh 'createdb -U postgres activerecord-ksuid_test' 41 | end 42 | 43 | desc 'Drops PostgreSQL database' 44 | task :drop do 45 | sh %(psql -d postgres -U postgres -c 'DROP DATABASE IF EXISTS "activerecord-ksuid_test"') 46 | end 47 | end 48 | end 49 | 50 | task spec: %i[spec:all] 51 | 52 | if ENV['APPRAISAL_INITIALIZED'] 53 | require 'rspec/core/rake_task' 54 | 55 | namespace :spec do 56 | task all: %i[mysql postgresql sqlite] 57 | 58 | task :mysql do 59 | driver = defined?(JRUBY_VERSION) ? 'mysql' : 'mysql2' 60 | sh "DRIVER=#{driver} DB_HOST=127.0.0.1 DB_USERNAME=root bundle exec rspec" 61 | end 62 | 63 | task :postgresql do 64 | sh 'DRIVER=postgresql DB_HOST=127.0.0.1 DB_USERNAME=postgres bundle exec rspec' 65 | end 66 | 67 | task :sqlite do 68 | sh 'DRIVER=sqlite3 DATABASE=":memory:" bundle exec rspec' 69 | end 70 | end 71 | else 72 | namespace :spec do 73 | task all: %i[mysql postgresql sqlite] 74 | 75 | task :mysql do 76 | driver = defined?(JRUBY_VERSION) ? 'mysql' : 'mysql2' 77 | run_rspec_with_driver(driver, { 'DB_HOST' => '127.0.0.1', 'DB_USERNAME' => 'root' }) 78 | end 79 | 80 | task :postgresql do 81 | run_rspec_with_driver('postgresql', { 'DB_HOST' => '127.0.0.1', 'DB_USERNAME' => 'postgres' }) 82 | end 83 | 84 | task :sqlite do 85 | run_rspec_with_driver('sqlite3', { 'DATABASE' => ':memory:' }) 86 | end 87 | 88 | def run_rspec_with_driver(driver, env = {}) 89 | command = String.new('rspec') 90 | env['DRIVER'] = driver 91 | if (gemfile = ENV['BUNDLE_GEMFILE']) && gemfile.match?(%r{gemfiles/}) # rubocop:disable Style/FetchEnvVar 92 | env['BUNDLE_GEMFILE'] = gemfile 93 | else 94 | command.prepend('appraisal rails-7.0 ') 95 | end 96 | success = system(env, command) 97 | 98 | abort "\nRSpec failed: #{$CHILD_STATUS}" unless success 99 | end 100 | end 101 | end 102 | 103 | with_optional_dependency do 104 | require 'yard-doctest' 105 | desc 'Run tests on the examples in documentation strings' 106 | task 'yard:doctest' do 107 | command = String.new('yard doctest') 108 | env = {} 109 | if (gemfile = ENV.fetch('BUNDLE_GEMFILE', nil)) 110 | env['BUNDLE_GEMFILE'] = gemfile 111 | elsif !ENV['APPRAISAL_INITIALIZED'] 112 | command.prepend('appraisal rails-7.0 ') 113 | end 114 | success = system(env, command) 115 | 116 | abort "\nYard Doctest failed: #{$CHILD_STATUS}" unless success 117 | end 118 | 119 | default << 'yard:doctest' 120 | end 121 | 122 | with_optional_dependency do 123 | require 'rubocop/rake_task' 124 | RuboCop::RakeTask.new(:rubocop) 125 | 126 | default << 'rubocop' 127 | end 128 | 129 | with_optional_dependency do 130 | require 'yard/rake/yardoc_task' 131 | YARD::Rake::YardocTask.new(:yard) 132 | 133 | default << 'yard' 134 | end 135 | 136 | with_optional_dependency do 137 | require 'inch/rake' 138 | Inch::Rake::Suggest.new(:inch) 139 | 140 | default << 'inch' 141 | end 142 | 143 | with_optional_dependency do 144 | require 'yardstick/rake/measurement' 145 | options = YAML.load_file('.yardstick.yml') 146 | Yardstick::Rake::Measurement.new(:yardstick_measure, options) do |measurement| 147 | measurement.output = 'coverage/docs.txt' 148 | end 149 | 150 | require 'yardstick/rake/verify' 151 | options = YAML.load_file('.yardstick.yml') 152 | Yardstick::Rake::Verify.new(:yardstick_verify, options) do |verify| 153 | verify.threshold = 100 154 | end 155 | 156 | task yardstick: %i[yardstick_measure yardstick_verify] 157 | end 158 | 159 | if ENV['CI'] 160 | task default: default 161 | elsif !ENV['APPRAISAL_INITIALIZED'] 162 | require 'appraisal/task' 163 | Appraisal::Task.new 164 | task default: default - %w[spec yard:doctest] + %w[appraisal] 165 | else 166 | ENV['COVERAGE'] = '1' 167 | task default: default & %w[spec yard:doctest] 168 | end 169 | -------------------------------------------------------------------------------- /activerecord-ksuid/UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading instructions for KSUID for ActiveRecord 2 | 3 | ## v1.0.0 4 | 5 | This is the initial release of this library. If you are upgrading from KSUID for Ruby 0.x, follow the notice below. 6 | 7 | ### Extracted `ActiveRecord::KSUID` into its own gem 8 | 9 | That KSUID for Ruby included ActiveRecord support directly in its gem has always been a regret of mine. It adds ActiveRecord and Rails concerns to a gem that you can use in any context. It makes running the test suite more complicated for no real gain. And it makes it kludgy to add support for more systems, like Sequel, since you have conflicting concerns in the same gem. 10 | 11 | To remove this problem, v1.0.0 extracts the ActiveRecord behavior into its own gem, `activerecord-ksuid`. This version is a straight extraction with an improved test suite so it _should_ mean that the only change you have to make when upgrading from v0.5.0 is to do the following in your Gemfile: 12 | 13 | ```diff 14 | - gem 'ksuid' 15 | + gem 'activerecord-ksuid' 16 | ``` 17 | 18 | If you are still on a version of KSUID for Ruby prior to v0.5.0, upgrade to that version first, solve the deprecation notice below, ensure your app still works, and then upgrade to v1.0.0. 19 | -------------------------------------------------------------------------------- /activerecord-ksuid/activerecord-ksuid.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path(File.join(__dir__, 'lib', 'active_record', 'ksuid', 'version')) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'activerecord-ksuid' 7 | spec.version = ActiveRecord::KSUID::VERSION 8 | spec.authors = ['Michael Herold'] 9 | spec.email = ['opensource@michaeljherold.com'] 10 | 11 | spec.summary = 'ActiveRecord integration for KSUIDs using the ksuid gem' 12 | spec.description = spec.summary 13 | spec.homepage = 'https://github.com/michaelherold/ksuid-ruby' 14 | spec.license = 'MIT' 15 | 16 | spec.files = %w[CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md UPGRADING.md] 17 | spec.files += %w[activerecord-ksuid.gemspec] 18 | spec.files += Dir['lib/**/*.rb'] 19 | spec.require_paths = ['lib'] 20 | 21 | spec.metadata['rubygems_mfa_required'] = 'true' 22 | 23 | spec.add_dependency 'activerecord', '>= 6.0' 24 | spec.add_dependency 'ksuid', '~> 1.0' 25 | 26 | spec.add_development_dependency 'bundler', '>= 1.15' 27 | end 28 | -------------------------------------------------------------------------------- /activerecord-ksuid/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'ksuid' 6 | require 'activerecord-ksuid' 7 | require 'irb' 8 | 9 | IRB.start(__FILE__) 10 | -------------------------------------------------------------------------------- /activerecord-ksuid/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | set -vx 6 | 7 | bundle install 8 | -------------------------------------------------------------------------------- /activerecord-ksuid/checksums/activerecord-ksuid-1.0.0.gem.sha512: -------------------------------------------------------------------------------- 1 | 343c96ea5e298826c112c0315022eabfaeec33d500d53aabdeb24331bcd872c8006b5c1fb6d529b2800f3e263df6a1cbcabb1e6a9dc2dec559e52d0f85fdd991 2 | -------------------------------------------------------------------------------- /activerecord-ksuid/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3" 3 | 4 | services: 5 | mysql: 6 | image: mysql:latest 7 | environment: 8 | - MYSQL_ALLOW_EMPTY_PASSWORD=yes 9 | - MYSQL_DATABASE=activerecord-ksuid_test 10 | - MYSQL_ROOT_PASSWORD= 11 | healthcheck: 12 | test: mysqladmin ping 13 | interval: 10s 14 | timeout: 5s 15 | retries: 3 16 | ports: 17 | - "3306:3306" 18 | 19 | postgres: 20 | image: postgres:latest 21 | environment: 22 | - POSTGRES_HOST_AUTH_METHOD=trust 23 | - POSTGRES_DB=activerecord-ksuid_test 24 | healthcheck: 25 | test: pg_isready -d activerecord-ksuid_test -h 127.0.0.1 -U postgres 26 | interval: 10s 27 | timeout: 5s 28 | retries: 5 29 | ports: 30 | - "5432:5432" 31 | -------------------------------------------------------------------------------- /activerecord-ksuid/gemfiles/rails_6.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ksuid", path: "../../ksuid" 6 | gem "rails", "~> 6.0.0" 7 | 8 | group :development do 9 | gem "benchmark-ips" 10 | gem "guard-bundler" 11 | gem "guard-inch" 12 | gem "guard-rspec" 13 | gem "guard-rubocop" 14 | gem "guard-yard" 15 | gem "yard", "~> 0.9" 16 | gem "yardstick" 17 | 18 | group :test do 19 | gem "appraisal" 20 | gem "pry" 21 | gem "rake" 22 | gem "rspec", "~> 3.6" 23 | gem "simplecov", "< 0.18", require: false 24 | 25 | group :linting do 26 | gem "yard-doctest" 27 | end 28 | end 29 | 30 | group :linting do 31 | gem "inch" 32 | gem "rubocop", "1.35.0" 33 | gem "rubocop-rake" 34 | gem "rubocop-rspec" 35 | end 36 | end 37 | 38 | platforms :jruby do 39 | gem "activerecord-jdbcmysql-adapter", "~> 60" 40 | gem "activerecord-jdbcpostgresql-adapter", "~> 60" 41 | gem "activerecord-jdbcsqlite3-adapter", "~> 60" 42 | end 43 | 44 | platforms :mri, :mingw, :x64_mingw do 45 | gem "mysql2", ">= 0.4.4" 46 | gem "pg", ">= 0.18", "< 2.0" 47 | gem "sqlite3", "~> 1.4" 48 | end 49 | 50 | gemspec path: "../" 51 | -------------------------------------------------------------------------------- /activerecord-ksuid/gemfiles/rails_6.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../../ksuid 3 | specs: 4 | ksuid (1.0.0) 5 | 6 | PATH 7 | remote: .. 8 | specs: 9 | activerecord-ksuid (1.0.0) 10 | activerecord (>= 6.0) 11 | ksuid (~> 1.0) 12 | 13 | GEM 14 | remote: https://rubygems.org/ 15 | specs: 16 | actioncable (6.0.6) 17 | actionpack (= 6.0.6) 18 | nio4r (~> 2.0) 19 | websocket-driver (>= 0.6.1) 20 | actionmailbox (6.0.6) 21 | actionpack (= 6.0.6) 22 | activejob (= 6.0.6) 23 | activerecord (= 6.0.6) 24 | activestorage (= 6.0.6) 25 | activesupport (= 6.0.6) 26 | mail (>= 2.7.1) 27 | actionmailer (6.0.6) 28 | actionpack (= 6.0.6) 29 | actionview (= 6.0.6) 30 | activejob (= 6.0.6) 31 | mail (~> 2.5, >= 2.5.4) 32 | rails-dom-testing (~> 2.0) 33 | actionpack (6.0.6) 34 | actionview (= 6.0.6) 35 | activesupport (= 6.0.6) 36 | rack (~> 2.0, >= 2.0.8) 37 | rack-test (>= 0.6.3) 38 | rails-dom-testing (~> 2.0) 39 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 40 | actiontext (6.0.6) 41 | actionpack (= 6.0.6) 42 | activerecord (= 6.0.6) 43 | activestorage (= 6.0.6) 44 | activesupport (= 6.0.6) 45 | nokogiri (>= 1.8.5) 46 | actionview (6.0.6) 47 | activesupport (= 6.0.6) 48 | builder (~> 3.1) 49 | erubi (~> 1.4) 50 | rails-dom-testing (~> 2.0) 51 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 52 | activejob (6.0.6) 53 | activesupport (= 6.0.6) 54 | globalid (>= 0.3.6) 55 | activemodel (6.0.6) 56 | activesupport (= 6.0.6) 57 | activerecord (6.0.6) 58 | activemodel (= 6.0.6) 59 | activesupport (= 6.0.6) 60 | activerecord-jdbc-adapter (60.4-java) 61 | activerecord (~> 6.0.0) 62 | activerecord-jdbcmysql-adapter (60.4-java) 63 | activerecord-jdbc-adapter (= 60.4) 64 | jdbc-mysql (>= 5.1.36, < 9) 65 | activerecord-jdbcpostgresql-adapter (60.4-java) 66 | activerecord-jdbc-adapter (= 60.4) 67 | jdbc-postgres (>= 9.4, < 43) 68 | activerecord-jdbcsqlite3-adapter (60.4-java) 69 | activerecord-jdbc-adapter (= 60.4) 70 | jdbc-sqlite3 (~> 3.8, < 3.30) 71 | activestorage (6.0.6) 72 | actionpack (= 6.0.6) 73 | activejob (= 6.0.6) 74 | activerecord (= 6.0.6) 75 | marcel (~> 1.0) 76 | activesupport (6.0.6) 77 | concurrent-ruby (~> 1.0, >= 1.0.2) 78 | i18n (>= 0.7, < 2) 79 | minitest (~> 5.1) 80 | tzinfo (~> 1.1) 81 | zeitwerk (~> 2.2, >= 2.2.2) 82 | appraisal (2.4.1) 83 | bundler 84 | rake 85 | thor (>= 0.14.0) 86 | ast (2.4.2) 87 | benchmark-ips (2.10.0) 88 | builder (3.2.4) 89 | coderay (1.1.3) 90 | concurrent-ruby (1.1.10) 91 | crass (1.0.6) 92 | diff-lcs (1.5.0) 93 | docile (1.4.0) 94 | erubi (1.11.0) 95 | ffi (1.15.5) 96 | ffi (1.15.5-java) 97 | formatador (1.1.0) 98 | globalid (1.0.0) 99 | activesupport (>= 5.0) 100 | guard (2.18.0) 101 | formatador (>= 0.2.4) 102 | listen (>= 2.7, < 4.0) 103 | lumberjack (>= 1.0.12, < 2.0) 104 | nenv (~> 0.1) 105 | notiffany (~> 0.0) 106 | pry (>= 0.13.0) 107 | shellany (~> 0.0) 108 | thor (>= 0.18.1) 109 | guard-bundler (3.0.0) 110 | bundler (>= 2.1, < 3) 111 | guard (~> 2.2) 112 | guard-compat (~> 1.1) 113 | guard-compat (1.2.1) 114 | guard-inch (0.2.0) 115 | guard (~> 2) 116 | inch (~> 0) 117 | guard-rspec (4.7.3) 118 | guard (~> 2.1) 119 | guard-compat (~> 1.1) 120 | rspec (>= 2.99.0, < 4.0) 121 | guard-rubocop (1.5.0) 122 | guard (~> 2.0) 123 | rubocop (< 2.0) 124 | guard-yard (2.2.1) 125 | guard (>= 1.1.0) 126 | yard (>= 0.7.0) 127 | i18n (1.12.0) 128 | concurrent-ruby (~> 1.0) 129 | inch (0.8.0) 130 | pry 131 | sparkr (>= 0.2.0) 132 | term-ansicolor 133 | yard (~> 0.9.12) 134 | jdbc-mysql (8.0.27) 135 | jdbc-postgres (42.2.25) 136 | jdbc-sqlite3 (3.28.0) 137 | json (2.6.2) 138 | json (2.6.2-java) 139 | listen (3.7.1) 140 | rb-fsevent (~> 0.10, >= 0.10.3) 141 | rb-inotify (~> 0.9, >= 0.9.10) 142 | loofah (2.19.0) 143 | crass (~> 1.0.2) 144 | nokogiri (>= 1.5.9) 145 | lumberjack (1.2.8) 146 | mail (2.7.1) 147 | mini_mime (>= 0.1.1) 148 | marcel (1.0.2) 149 | method_source (1.0.0) 150 | mini_mime (1.1.2) 151 | mini_portile2 (2.8.0) 152 | minitest (5.16.3) 153 | mysql2 (0.5.4) 154 | nenv (0.3.0) 155 | nio4r (2.5.8) 156 | nio4r (2.5.8-java) 157 | nokogiri (1.13.8-java) 158 | racc (~> 1.4) 159 | nokogiri (1.13.8-x86_64-darwin) 160 | racc (~> 1.4) 161 | notiffany (0.1.3) 162 | nenv (~> 0.1) 163 | shellany (~> 0.0) 164 | parallel (1.22.1) 165 | parser (3.1.2.1) 166 | ast (~> 2.4.1) 167 | pg (1.4.4) 168 | pry (0.14.1) 169 | coderay (~> 1.1) 170 | method_source (~> 1.0) 171 | pry (0.14.1-java) 172 | coderay (~> 1.1) 173 | method_source (~> 1.0) 174 | spoon (~> 0.0) 175 | racc (1.6.0) 176 | racc (1.6.0-java) 177 | rack (2.2.4) 178 | rack-test (2.0.2) 179 | rack (>= 1.3) 180 | rails (6.0.6) 181 | actioncable (= 6.0.6) 182 | actionmailbox (= 6.0.6) 183 | actionmailer (= 6.0.6) 184 | actionpack (= 6.0.6) 185 | actiontext (= 6.0.6) 186 | actionview (= 6.0.6) 187 | activejob (= 6.0.6) 188 | activemodel (= 6.0.6) 189 | activerecord (= 6.0.6) 190 | activestorage (= 6.0.6) 191 | activesupport (= 6.0.6) 192 | bundler (>= 1.3.0) 193 | railties (= 6.0.6) 194 | sprockets-rails (>= 2.0.0) 195 | rails-dom-testing (2.0.3) 196 | activesupport (>= 4.2.0) 197 | nokogiri (>= 1.6) 198 | rails-html-sanitizer (1.4.3) 199 | loofah (~> 2.3) 200 | railties (6.0.6) 201 | actionpack (= 6.0.6) 202 | activesupport (= 6.0.6) 203 | method_source 204 | rake (>= 0.8.7) 205 | thor (>= 0.20.3, < 2.0) 206 | rainbow (3.1.1) 207 | rake (13.0.6) 208 | rb-fsevent (0.11.2) 209 | rb-inotify (0.10.1) 210 | ffi (~> 1.0) 211 | regexp_parser (2.6.0) 212 | rexml (3.2.5) 213 | rspec (3.11.0) 214 | rspec-core (~> 3.11.0) 215 | rspec-expectations (~> 3.11.0) 216 | rspec-mocks (~> 3.11.0) 217 | rspec-core (3.11.0) 218 | rspec-support (~> 3.11.0) 219 | rspec-expectations (3.11.1) 220 | diff-lcs (>= 1.2.0, < 2.0) 221 | rspec-support (~> 3.11.0) 222 | rspec-mocks (3.11.1) 223 | diff-lcs (>= 1.2.0, < 2.0) 224 | rspec-support (~> 3.11.0) 225 | rspec-support (3.11.1) 226 | rubocop (1.35.0) 227 | json (~> 2.3) 228 | parallel (~> 1.10) 229 | parser (>= 3.1.2.1) 230 | rainbow (>= 2.2.2, < 4.0) 231 | regexp_parser (>= 1.8, < 3.0) 232 | rexml (>= 3.2.5, < 4.0) 233 | rubocop-ast (>= 1.20.1, < 2.0) 234 | ruby-progressbar (~> 1.7) 235 | unicode-display_width (>= 1.4.0, < 3.0) 236 | rubocop-ast (1.21.0) 237 | parser (>= 3.1.1.0) 238 | rubocop-rake (0.6.0) 239 | rubocop (~> 1.0) 240 | rubocop-rspec (2.13.2) 241 | rubocop (~> 1.33) 242 | ruby-progressbar (1.11.0) 243 | shellany (0.0.1) 244 | simplecov (0.17.1) 245 | docile (~> 1.1) 246 | json (>= 1.8, < 3) 247 | simplecov-html (~> 0.10.0) 248 | simplecov-html (0.10.2) 249 | sparkr (0.4.1) 250 | spoon (0.0.6) 251 | ffi 252 | sprockets (4.1.1) 253 | concurrent-ruby (~> 1.0) 254 | rack (> 1, < 3) 255 | sprockets-rails (3.4.2) 256 | actionpack (>= 5.2) 257 | activesupport (>= 5.2) 258 | sprockets (>= 3.0.0) 259 | sqlite3 (1.5.3) 260 | mini_portile2 (~> 2.8.0) 261 | sqlite3 (1.5.3-x86_64-darwin) 262 | sync (0.5.0) 263 | term-ansicolor (1.7.1) 264 | tins (~> 1.0) 265 | thor (1.2.1) 266 | thread_safe (0.3.6) 267 | thread_safe (0.3.6-java) 268 | tins (1.31.1) 269 | sync 270 | tzinfo (1.2.10) 271 | thread_safe (~> 0.1) 272 | unicode-display_width (2.3.0) 273 | webrick (1.7.0) 274 | websocket-driver (0.7.5) 275 | websocket-extensions (>= 0.1.0) 276 | websocket-driver (0.7.5-java) 277 | websocket-extensions (>= 0.1.0) 278 | websocket-extensions (0.1.5) 279 | yard (0.9.28) 280 | webrick (~> 1.7.0) 281 | yard-doctest (0.1.17) 282 | minitest 283 | yard 284 | yardstick (0.9.9) 285 | yard (~> 0.8, >= 0.8.7.2) 286 | zeitwerk (2.6.1) 287 | 288 | PLATFORMS 289 | universal-java-11 290 | x86_64-darwin-21 291 | 292 | DEPENDENCIES 293 | activerecord-jdbcmysql-adapter (~> 60) 294 | activerecord-jdbcpostgresql-adapter (~> 60) 295 | activerecord-jdbcsqlite3-adapter (~> 60) 296 | activerecord-ksuid! 297 | appraisal 298 | benchmark-ips 299 | bundler (>= 1.15) 300 | guard-bundler 301 | guard-inch 302 | guard-rspec 303 | guard-rubocop 304 | guard-yard 305 | inch 306 | ksuid! 307 | mysql2 (>= 0.4.4) 308 | pg (>= 0.18, < 2.0) 309 | pry 310 | rails (~> 6.0.0) 311 | rake 312 | rspec (~> 3.6) 313 | rubocop (= 1.35.0) 314 | rubocop-rake 315 | rubocop-rspec 316 | simplecov (< 0.18) 317 | sqlite3 (~> 1.4) 318 | yard (~> 0.9) 319 | yard-doctest 320 | yardstick 321 | 322 | BUNDLED WITH 323 | 2.3.23 324 | -------------------------------------------------------------------------------- /activerecord-ksuid/gemfiles/rails_6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ksuid", path: "../../ksuid" 6 | gem "rails", "~> 6.1.0" 7 | 8 | group :development do 9 | gem "benchmark-ips" 10 | gem "guard-bundler" 11 | gem "guard-inch" 12 | gem "guard-rspec" 13 | gem "guard-rubocop" 14 | gem "guard-yard" 15 | gem "yard", "~> 0.9" 16 | gem "yardstick" 17 | 18 | group :test do 19 | gem "appraisal" 20 | gem "pry" 21 | gem "rake" 22 | gem "rspec", "~> 3.6" 23 | gem "simplecov", "< 0.18", require: false 24 | 25 | group :linting do 26 | gem "yard-doctest" 27 | end 28 | end 29 | 30 | group :linting do 31 | gem "inch" 32 | gem "rubocop", "1.35.0" 33 | gem "rubocop-rake" 34 | gem "rubocop-rspec" 35 | end 36 | end 37 | 38 | platforms :jruby do 39 | gem "activerecord-jdbcmysql-adapter", "~> 61" 40 | gem "activerecord-jdbcpostgresql-adapter", "~> 61" 41 | gem "activerecord-jdbcsqlite3-adapter", "~> 61" 42 | end 43 | 44 | platforms :mri, :mingw, :x64_mingw do 45 | gem "mysql2", "~> 0.5" 46 | gem "pg", "~> 1.1" 47 | gem "sqlite3", "~> 1.4" 48 | end 49 | 50 | gemspec path: "../" 51 | -------------------------------------------------------------------------------- /activerecord-ksuid/gemfiles/rails_6.1.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../../ksuid 3 | specs: 4 | ksuid (1.0.0) 5 | 6 | PATH 7 | remote: .. 8 | specs: 9 | activerecord-ksuid (1.0.0) 10 | activerecord (>= 6.0) 11 | ksuid (~> 1.0) 12 | 13 | GEM 14 | remote: https://rubygems.org/ 15 | specs: 16 | actioncable (6.1.7) 17 | actionpack (= 6.1.7) 18 | activesupport (= 6.1.7) 19 | nio4r (~> 2.0) 20 | websocket-driver (>= 0.6.1) 21 | actionmailbox (6.1.7) 22 | actionpack (= 6.1.7) 23 | activejob (= 6.1.7) 24 | activerecord (= 6.1.7) 25 | activestorage (= 6.1.7) 26 | activesupport (= 6.1.7) 27 | mail (>= 2.7.1) 28 | actionmailer (6.1.7) 29 | actionpack (= 6.1.7) 30 | actionview (= 6.1.7) 31 | activejob (= 6.1.7) 32 | activesupport (= 6.1.7) 33 | mail (~> 2.5, >= 2.5.4) 34 | rails-dom-testing (~> 2.0) 35 | actionpack (6.1.7) 36 | actionview (= 6.1.7) 37 | activesupport (= 6.1.7) 38 | rack (~> 2.0, >= 2.0.9) 39 | rack-test (>= 0.6.3) 40 | rails-dom-testing (~> 2.0) 41 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 42 | actiontext (6.1.7) 43 | actionpack (= 6.1.7) 44 | activerecord (= 6.1.7) 45 | activestorage (= 6.1.7) 46 | activesupport (= 6.1.7) 47 | nokogiri (>= 1.8.5) 48 | actionview (6.1.7) 49 | activesupport (= 6.1.7) 50 | builder (~> 3.1) 51 | erubi (~> 1.4) 52 | rails-dom-testing (~> 2.0) 53 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 54 | activejob (6.1.7) 55 | activesupport (= 6.1.7) 56 | globalid (>= 0.3.6) 57 | activemodel (6.1.7) 58 | activesupport (= 6.1.7) 59 | activerecord (6.1.7) 60 | activemodel (= 6.1.7) 61 | activesupport (= 6.1.7) 62 | activerecord-jdbc-adapter (61.2-java) 63 | activerecord (~> 6.1.0) 64 | activerecord-jdbcmysql-adapter (61.2-java) 65 | activerecord-jdbc-adapter (= 61.2) 66 | jdbc-mysql (>= 5.1.36, < 9) 67 | activerecord-jdbcpostgresql-adapter (61.2-java) 68 | activerecord-jdbc-adapter (= 61.2) 69 | jdbc-postgres (>= 9.4, < 43) 70 | activerecord-jdbcsqlite3-adapter (61.2-java) 71 | activerecord-jdbc-adapter (= 61.2) 72 | jdbc-sqlite3 (~> 3.8, < 3.30) 73 | activestorage (6.1.7) 74 | actionpack (= 6.1.7) 75 | activejob (= 6.1.7) 76 | activerecord (= 6.1.7) 77 | activesupport (= 6.1.7) 78 | marcel (~> 1.0) 79 | mini_mime (>= 1.1.0) 80 | activesupport (6.1.7) 81 | concurrent-ruby (~> 1.0, >= 1.0.2) 82 | i18n (>= 1.6, < 2) 83 | minitest (>= 5.1) 84 | tzinfo (~> 2.0) 85 | zeitwerk (~> 2.3) 86 | appraisal (2.4.1) 87 | bundler 88 | rake 89 | thor (>= 0.14.0) 90 | ast (2.4.2) 91 | benchmark-ips (2.10.0) 92 | builder (3.2.4) 93 | coderay (1.1.3) 94 | concurrent-ruby (1.1.10) 95 | crass (1.0.6) 96 | diff-lcs (1.5.0) 97 | docile (1.4.0) 98 | erubi (1.11.0) 99 | ffi (1.15.5) 100 | ffi (1.15.5-java) 101 | formatador (1.1.0) 102 | globalid (1.0.0) 103 | activesupport (>= 5.0) 104 | guard (2.18.0) 105 | formatador (>= 0.2.4) 106 | listen (>= 2.7, < 4.0) 107 | lumberjack (>= 1.0.12, < 2.0) 108 | nenv (~> 0.1) 109 | notiffany (~> 0.0) 110 | pry (>= 0.13.0) 111 | shellany (~> 0.0) 112 | thor (>= 0.18.1) 113 | guard-bundler (3.0.0) 114 | bundler (>= 2.1, < 3) 115 | guard (~> 2.2) 116 | guard-compat (~> 1.1) 117 | guard-compat (1.2.1) 118 | guard-inch (0.2.0) 119 | guard (~> 2) 120 | inch (~> 0) 121 | guard-rspec (4.7.3) 122 | guard (~> 2.1) 123 | guard-compat (~> 1.1) 124 | rspec (>= 2.99.0, < 4.0) 125 | guard-rubocop (1.5.0) 126 | guard (~> 2.0) 127 | rubocop (< 2.0) 128 | guard-yard (2.2.1) 129 | guard (>= 1.1.0) 130 | yard (>= 0.7.0) 131 | i18n (1.12.0) 132 | concurrent-ruby (~> 1.0) 133 | inch (0.8.0) 134 | pry 135 | sparkr (>= 0.2.0) 136 | term-ansicolor 137 | yard (~> 0.9.12) 138 | jdbc-mysql (8.0.27) 139 | jdbc-postgres (42.2.25) 140 | jdbc-sqlite3 (3.28.0) 141 | json (2.6.2) 142 | json (2.6.2-java) 143 | listen (3.7.1) 144 | rb-fsevent (~> 0.10, >= 0.10.3) 145 | rb-inotify (~> 0.9, >= 0.9.10) 146 | loofah (2.19.0) 147 | crass (~> 1.0.2) 148 | nokogiri (>= 1.5.9) 149 | lumberjack (1.2.8) 150 | mail (2.7.1) 151 | mini_mime (>= 0.1.1) 152 | marcel (1.0.2) 153 | method_source (1.0.0) 154 | mini_mime (1.1.2) 155 | mini_portile2 (2.8.0) 156 | minitest (5.16.3) 157 | mysql2 (0.5.4) 158 | nenv (0.3.0) 159 | nio4r (2.5.8) 160 | nio4r (2.5.8-java) 161 | nokogiri (1.13.8-java) 162 | racc (~> 1.4) 163 | nokogiri (1.13.8-x86_64-darwin) 164 | racc (~> 1.4) 165 | notiffany (0.1.3) 166 | nenv (~> 0.1) 167 | shellany (~> 0.0) 168 | parallel (1.22.1) 169 | parser (3.1.2.1) 170 | ast (~> 2.4.1) 171 | pg (1.4.4) 172 | pry (0.14.1) 173 | coderay (~> 1.1) 174 | method_source (~> 1.0) 175 | pry (0.14.1-java) 176 | coderay (~> 1.1) 177 | method_source (~> 1.0) 178 | spoon (~> 0.0) 179 | racc (1.6.0) 180 | racc (1.6.0-java) 181 | rack (2.2.4) 182 | rack-test (2.0.2) 183 | rack (>= 1.3) 184 | rails (6.1.7) 185 | actioncable (= 6.1.7) 186 | actionmailbox (= 6.1.7) 187 | actionmailer (= 6.1.7) 188 | actionpack (= 6.1.7) 189 | actiontext (= 6.1.7) 190 | actionview (= 6.1.7) 191 | activejob (= 6.1.7) 192 | activemodel (= 6.1.7) 193 | activerecord (= 6.1.7) 194 | activestorage (= 6.1.7) 195 | activesupport (= 6.1.7) 196 | bundler (>= 1.15.0) 197 | railties (= 6.1.7) 198 | sprockets-rails (>= 2.0.0) 199 | rails-dom-testing (2.0.3) 200 | activesupport (>= 4.2.0) 201 | nokogiri (>= 1.6) 202 | rails-html-sanitizer (1.4.3) 203 | loofah (~> 2.3) 204 | railties (6.1.7) 205 | actionpack (= 6.1.7) 206 | activesupport (= 6.1.7) 207 | method_source 208 | rake (>= 12.2) 209 | thor (~> 1.0) 210 | rainbow (3.1.1) 211 | rake (13.0.6) 212 | rb-fsevent (0.11.2) 213 | rb-inotify (0.10.1) 214 | ffi (~> 1.0) 215 | regexp_parser (2.6.0) 216 | rexml (3.2.5) 217 | rspec (3.11.0) 218 | rspec-core (~> 3.11.0) 219 | rspec-expectations (~> 3.11.0) 220 | rspec-mocks (~> 3.11.0) 221 | rspec-core (3.11.0) 222 | rspec-support (~> 3.11.0) 223 | rspec-expectations (3.11.1) 224 | diff-lcs (>= 1.2.0, < 2.0) 225 | rspec-support (~> 3.11.0) 226 | rspec-mocks (3.11.1) 227 | diff-lcs (>= 1.2.0, < 2.0) 228 | rspec-support (~> 3.11.0) 229 | rspec-support (3.11.1) 230 | rubocop (1.35.0) 231 | json (~> 2.3) 232 | parallel (~> 1.10) 233 | parser (>= 3.1.2.1) 234 | rainbow (>= 2.2.2, < 4.0) 235 | regexp_parser (>= 1.8, < 3.0) 236 | rexml (>= 3.2.5, < 4.0) 237 | rubocop-ast (>= 1.20.1, < 2.0) 238 | ruby-progressbar (~> 1.7) 239 | unicode-display_width (>= 1.4.0, < 3.0) 240 | rubocop-ast (1.21.0) 241 | parser (>= 3.1.1.0) 242 | rubocop-rake (0.6.0) 243 | rubocop (~> 1.0) 244 | rubocop-rspec (2.13.2) 245 | rubocop (~> 1.33) 246 | ruby-progressbar (1.11.0) 247 | shellany (0.0.1) 248 | simplecov (0.17.1) 249 | docile (~> 1.1) 250 | json (>= 1.8, < 3) 251 | simplecov-html (~> 0.10.0) 252 | simplecov-html (0.10.2) 253 | sparkr (0.4.1) 254 | spoon (0.0.6) 255 | ffi 256 | sprockets (4.1.1) 257 | concurrent-ruby (~> 1.0) 258 | rack (> 1, < 3) 259 | sprockets-rails (3.4.2) 260 | actionpack (>= 5.2) 261 | activesupport (>= 5.2) 262 | sprockets (>= 3.0.0) 263 | sqlite3 (1.5.3) 264 | mini_portile2 (~> 2.8.0) 265 | sqlite3 (1.5.3-x86_64-darwin) 266 | sync (0.5.0) 267 | term-ansicolor (1.7.1) 268 | tins (~> 1.0) 269 | thor (1.2.1) 270 | tins (1.31.1) 271 | sync 272 | tzinfo (2.0.5) 273 | concurrent-ruby (~> 1.0) 274 | unicode-display_width (2.3.0) 275 | webrick (1.7.0) 276 | websocket-driver (0.7.5) 277 | websocket-extensions (>= 0.1.0) 278 | websocket-driver (0.7.5-java) 279 | websocket-extensions (>= 0.1.0) 280 | websocket-extensions (0.1.5) 281 | yard (0.9.28) 282 | webrick (~> 1.7.0) 283 | yard-doctest (0.1.17) 284 | minitest 285 | yard 286 | yardstick (0.9.9) 287 | yard (~> 0.8, >= 0.8.7.2) 288 | zeitwerk (2.6.1) 289 | 290 | PLATFORMS 291 | universal-java-11 292 | x86_64-darwin-21 293 | 294 | DEPENDENCIES 295 | activerecord-jdbcmysql-adapter (~> 61) 296 | activerecord-jdbcpostgresql-adapter (~> 61) 297 | activerecord-jdbcsqlite3-adapter (~> 61) 298 | activerecord-ksuid! 299 | appraisal 300 | benchmark-ips 301 | bundler (>= 1.15) 302 | guard-bundler 303 | guard-inch 304 | guard-rspec 305 | guard-rubocop 306 | guard-yard 307 | inch 308 | ksuid! 309 | mysql2 (~> 0.5) 310 | pg (~> 1.1) 311 | pry 312 | rails (~> 6.1.0) 313 | rake 314 | rspec (~> 3.6) 315 | rubocop (= 1.35.0) 316 | rubocop-rake 317 | rubocop-rspec 318 | simplecov (< 0.18) 319 | sqlite3 (~> 1.4) 320 | yard (~> 0.9) 321 | yard-doctest 322 | yardstick 323 | 324 | BUNDLED WITH 325 | 2.3.23 326 | -------------------------------------------------------------------------------- /activerecord-ksuid/gemfiles/rails_7.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ksuid", path: "../../ksuid" 6 | gem "rails", "~> 7.0.0" 7 | 8 | group :development do 9 | gem "benchmark-ips" 10 | gem "guard-bundler" 11 | gem "guard-inch" 12 | gem "guard-rspec" 13 | gem "guard-rubocop" 14 | gem "guard-yard" 15 | gem "yard", "~> 0.9" 16 | gem "yardstick" 17 | 18 | group :test do 19 | gem "appraisal" 20 | gem "pry" 21 | gem "rake" 22 | gem "rspec", "~> 3.6" 23 | gem "simplecov", "< 0.18", require: false 24 | 25 | group :linting do 26 | gem "yard-doctest" 27 | end 28 | end 29 | 30 | group :linting do 31 | gem "inch" 32 | gem "rubocop", "1.35.0" 33 | gem "rubocop-rake" 34 | gem "rubocop-rspec" 35 | end 36 | end 37 | 38 | platforms :mri, :mingw, :x64_mingw do 39 | gem "mysql2", "~> 0.5" 40 | gem "pg", "~> 1.1" 41 | gem "sqlite3", "~> 1.4" 42 | end 43 | 44 | gemspec path: "../" 45 | -------------------------------------------------------------------------------- /activerecord-ksuid/gemfiles/rails_7.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../../ksuid 3 | specs: 4 | ksuid (1.0.0) 5 | 6 | PATH 7 | remote: .. 8 | specs: 9 | activerecord-ksuid (1.0.0) 10 | activerecord (>= 6.0) 11 | ksuid (~> 1.0) 12 | 13 | GEM 14 | remote: https://rubygems.org/ 15 | specs: 16 | actioncable (7.0.3.1) 17 | actionpack (= 7.0.3.1) 18 | activesupport (= 7.0.3.1) 19 | nio4r (~> 2.0) 20 | websocket-driver (>= 0.6.1) 21 | actionmailbox (7.0.3.1) 22 | actionpack (= 7.0.3.1) 23 | activejob (= 7.0.3.1) 24 | activerecord (= 7.0.3.1) 25 | activestorage (= 7.0.3.1) 26 | activesupport (= 7.0.3.1) 27 | mail (>= 2.7.1) 28 | net-imap 29 | net-pop 30 | net-smtp 31 | actionmailer (7.0.3.1) 32 | actionpack (= 7.0.3.1) 33 | actionview (= 7.0.3.1) 34 | activejob (= 7.0.3.1) 35 | activesupport (= 7.0.3.1) 36 | mail (~> 2.5, >= 2.5.4) 37 | net-imap 38 | net-pop 39 | net-smtp 40 | rails-dom-testing (~> 2.0) 41 | actionpack (7.0.3.1) 42 | actionview (= 7.0.3.1) 43 | activesupport (= 7.0.3.1) 44 | rack (~> 2.0, >= 2.2.0) 45 | rack-test (>= 0.6.3) 46 | rails-dom-testing (~> 2.0) 47 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 48 | actiontext (7.0.3.1) 49 | actionpack (= 7.0.3.1) 50 | activerecord (= 7.0.3.1) 51 | activestorage (= 7.0.3.1) 52 | activesupport (= 7.0.3.1) 53 | globalid (>= 0.6.0) 54 | nokogiri (>= 1.8.5) 55 | actionview (7.0.3.1) 56 | activesupport (= 7.0.3.1) 57 | builder (~> 3.1) 58 | erubi (~> 1.4) 59 | rails-dom-testing (~> 2.0) 60 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 61 | activejob (7.0.3.1) 62 | activesupport (= 7.0.3.1) 63 | globalid (>= 0.3.6) 64 | activemodel (7.0.3.1) 65 | activesupport (= 7.0.3.1) 66 | activerecord (7.0.3.1) 67 | activemodel (= 7.0.3.1) 68 | activesupport (= 7.0.3.1) 69 | activestorage (7.0.3.1) 70 | actionpack (= 7.0.3.1) 71 | activejob (= 7.0.3.1) 72 | activerecord (= 7.0.3.1) 73 | activesupport (= 7.0.3.1) 74 | marcel (~> 1.0) 75 | mini_mime (>= 1.1.0) 76 | activesupport (7.0.3.1) 77 | concurrent-ruby (~> 1.0, >= 1.0.2) 78 | i18n (>= 1.6, < 2) 79 | minitest (>= 5.1) 80 | tzinfo (~> 2.0) 81 | appraisal (2.4.1) 82 | bundler 83 | rake 84 | thor (>= 0.14.0) 85 | ast (2.4.2) 86 | benchmark-ips (2.10.0) 87 | builder (3.2.4) 88 | coderay (1.1.3) 89 | concurrent-ruby (1.1.10) 90 | crass (1.0.6) 91 | diff-lcs (1.5.0) 92 | digest (3.1.0) 93 | docile (1.4.0) 94 | erubi (1.11.0) 95 | ffi (1.15.5) 96 | formatador (1.1.0) 97 | globalid (1.0.0) 98 | activesupport (>= 5.0) 99 | guard (2.18.0) 100 | formatador (>= 0.2.4) 101 | listen (>= 2.7, < 4.0) 102 | lumberjack (>= 1.0.12, < 2.0) 103 | nenv (~> 0.1) 104 | notiffany (~> 0.0) 105 | pry (>= 0.13.0) 106 | shellany (~> 0.0) 107 | thor (>= 0.18.1) 108 | guard-bundler (3.0.0) 109 | bundler (>= 2.1, < 3) 110 | guard (~> 2.2) 111 | guard-compat (~> 1.1) 112 | guard-compat (1.2.1) 113 | guard-inch (0.2.0) 114 | guard (~> 2) 115 | inch (~> 0) 116 | guard-rspec (4.7.3) 117 | guard (~> 2.1) 118 | guard-compat (~> 1.1) 119 | rspec (>= 2.99.0, < 4.0) 120 | guard-rubocop (1.5.0) 121 | guard (~> 2.0) 122 | rubocop (< 2.0) 123 | guard-yard (2.2.1) 124 | guard (>= 1.1.0) 125 | yard (>= 0.7.0) 126 | i18n (1.12.0) 127 | concurrent-ruby (~> 1.0) 128 | inch (0.8.0) 129 | pry 130 | sparkr (>= 0.2.0) 131 | term-ansicolor 132 | yard (~> 0.9.12) 133 | json (2.6.2) 134 | listen (3.7.1) 135 | rb-fsevent (~> 0.10, >= 0.10.3) 136 | rb-inotify (~> 0.9, >= 0.9.10) 137 | loofah (2.19.0) 138 | crass (~> 1.0.2) 139 | nokogiri (>= 1.5.9) 140 | lumberjack (1.2.8) 141 | mail (2.7.1) 142 | mini_mime (>= 0.1.1) 143 | marcel (1.0.2) 144 | method_source (1.0.0) 145 | mini_mime (1.1.2) 146 | minitest (5.16.3) 147 | mysql2 (0.5.4) 148 | nenv (0.3.0) 149 | net-imap (0.2.3) 150 | digest 151 | net-protocol 152 | strscan 153 | net-pop (0.1.1) 154 | digest 155 | net-protocol 156 | timeout 157 | net-protocol (0.1.3) 158 | timeout 159 | net-smtp (0.3.1) 160 | digest 161 | net-protocol 162 | timeout 163 | nio4r (2.5.8) 164 | nokogiri (1.13.8-x86_64-darwin) 165 | racc (~> 1.4) 166 | notiffany (0.1.3) 167 | nenv (~> 0.1) 168 | shellany (~> 0.0) 169 | parallel (1.22.1) 170 | parser (3.1.2.1) 171 | ast (~> 2.4.1) 172 | pg (1.4.4) 173 | pry (0.14.1) 174 | coderay (~> 1.1) 175 | method_source (~> 1.0) 176 | racc (1.6.0) 177 | rack (2.2.4) 178 | rack-test (2.0.2) 179 | rack (>= 1.3) 180 | rails (7.0.3.1) 181 | actioncable (= 7.0.3.1) 182 | actionmailbox (= 7.0.3.1) 183 | actionmailer (= 7.0.3.1) 184 | actionpack (= 7.0.3.1) 185 | actiontext (= 7.0.3.1) 186 | actionview (= 7.0.3.1) 187 | activejob (= 7.0.3.1) 188 | activemodel (= 7.0.3.1) 189 | activerecord (= 7.0.3.1) 190 | activestorage (= 7.0.3.1) 191 | activesupport (= 7.0.3.1) 192 | bundler (>= 1.15.0) 193 | railties (= 7.0.3.1) 194 | rails-dom-testing (2.0.3) 195 | activesupport (>= 4.2.0) 196 | nokogiri (>= 1.6) 197 | rails-html-sanitizer (1.4.3) 198 | loofah (~> 2.3) 199 | railties (7.0.3.1) 200 | actionpack (= 7.0.3.1) 201 | activesupport (= 7.0.3.1) 202 | method_source 203 | rake (>= 12.2) 204 | thor (~> 1.0) 205 | zeitwerk (~> 2.5) 206 | rainbow (3.1.1) 207 | rake (13.0.6) 208 | rb-fsevent (0.11.2) 209 | rb-inotify (0.10.1) 210 | ffi (~> 1.0) 211 | regexp_parser (2.6.0) 212 | rexml (3.2.5) 213 | rspec (3.11.0) 214 | rspec-core (~> 3.11.0) 215 | rspec-expectations (~> 3.11.0) 216 | rspec-mocks (~> 3.11.0) 217 | rspec-core (3.11.0) 218 | rspec-support (~> 3.11.0) 219 | rspec-expectations (3.11.1) 220 | diff-lcs (>= 1.2.0, < 2.0) 221 | rspec-support (~> 3.11.0) 222 | rspec-mocks (3.11.1) 223 | diff-lcs (>= 1.2.0, < 2.0) 224 | rspec-support (~> 3.11.0) 225 | rspec-support (3.11.1) 226 | rubocop (1.35.0) 227 | json (~> 2.3) 228 | parallel (~> 1.10) 229 | parser (>= 3.1.2.1) 230 | rainbow (>= 2.2.2, < 4.0) 231 | regexp_parser (>= 1.8, < 3.0) 232 | rexml (>= 3.2.5, < 4.0) 233 | rubocop-ast (>= 1.20.1, < 2.0) 234 | ruby-progressbar (~> 1.7) 235 | unicode-display_width (>= 1.4.0, < 3.0) 236 | rubocop-ast (1.21.0) 237 | parser (>= 3.1.1.0) 238 | rubocop-rake (0.6.0) 239 | rubocop (~> 1.0) 240 | rubocop-rspec (2.13.2) 241 | rubocop (~> 1.33) 242 | ruby-progressbar (1.11.0) 243 | shellany (0.0.1) 244 | simplecov (0.17.1) 245 | docile (~> 1.1) 246 | json (>= 1.8, < 3) 247 | simplecov-html (~> 0.10.0) 248 | simplecov-html (0.10.2) 249 | sparkr (0.4.1) 250 | sqlite3 (1.5.3-x86_64-darwin) 251 | strscan (3.0.4) 252 | sync (0.5.0) 253 | term-ansicolor (1.7.1) 254 | tins (~> 1.0) 255 | thor (1.2.1) 256 | timeout (0.3.0) 257 | tins (1.31.1) 258 | sync 259 | tzinfo (2.0.5) 260 | concurrent-ruby (~> 1.0) 261 | unicode-display_width (2.3.0) 262 | webrick (1.7.0) 263 | websocket-driver (0.7.5) 264 | websocket-extensions (>= 0.1.0) 265 | websocket-extensions (0.1.5) 266 | yard (0.9.28) 267 | webrick (~> 1.7.0) 268 | yard-doctest (0.1.17) 269 | minitest 270 | yard 271 | yardstick (0.9.9) 272 | yard (~> 0.8, >= 0.8.7.2) 273 | zeitwerk (2.6.1) 274 | 275 | PLATFORMS 276 | x86_64-darwin-21 277 | 278 | DEPENDENCIES 279 | activerecord-ksuid! 280 | appraisal 281 | benchmark-ips 282 | bundler (>= 1.15) 283 | guard-bundler 284 | guard-inch 285 | guard-rspec 286 | guard-rubocop 287 | guard-yard 288 | inch 289 | ksuid! 290 | mysql2 (~> 0.5) 291 | pg (~> 1.1) 292 | pry 293 | rails (~> 7.0.0) 294 | rake 295 | rspec (~> 3.6) 296 | rubocop (= 1.35.0) 297 | rubocop-rake 298 | rubocop-rspec 299 | simplecov (< 0.18) 300 | sqlite3 (~> 1.4) 301 | yard (~> 0.9) 302 | yard-doctest 303 | yardstick 304 | 305 | BUNDLED WITH 306 | 2.3.23 307 | -------------------------------------------------------------------------------- /activerecord-ksuid/lib/active_record/ksuid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_record/ksuid/binary_type' 4 | require 'active_record/ksuid/prefixed_type' 5 | require 'active_record/ksuid/type' 6 | 7 | # The Ruby on Rails object-relational mapper 8 | # 9 | # @see https://guides.rubyonrails.org/ Ruby on Rails documentation 10 | module ActiveRecord 11 | # Enables an Active Record model to have a KSUID attribute 12 | # 13 | # @api public 14 | # @since 0.5.0 15 | module KSUID 16 | # Builds a module to include into the model 17 | # 18 | # @api public 19 | # 20 | # @example Add a `#ksuid` attribute to a model 21 | # class Event < ActiveRecord::Base 22 | # include ActiveRecord::KSUID[:ksuid] 23 | # end 24 | # 25 | # @example Add a `#remote_id` attribute to a model and overrides `#created_at` to use the KSUID 26 | # class Event < ActiveRecord::Base 27 | # include ActiveRecord::KSUID[:remote_id, created_at: true] 28 | # end 29 | # 30 | # @example Add a prefixed `#ksuid` attribute to a model 31 | # class Event < ActiveRecord::Base 32 | # include ActiveRecord::KSUID[:ksuid, prefix: 'evt_'] 33 | # end 34 | # 35 | # @param auto_gen [Boolean] whether to generate a KSUID upon initialization 36 | # @param field [String, Symbol] the name of the field to use as a KSUID 37 | # @param created_at [Boolean] whether to override the `#created_at` method 38 | # @param binary [Boolean] whether to store the KSUID as a binary or a string 39 | # @param prefix [String, nil] a prefix to prepend to the KSUID attribute 40 | # @return [Module] the module to include into the model 41 | def self.[](field, auto_gen: true, created_at: false, binary: false, prefix: nil) 42 | raise ArgumentError, 'cannot include a prefix on a binary KSUID' if binary && prefix 43 | 44 | Module.new.tap do |mod| 45 | if prefix 46 | define_prefixed_attribute(field, mod, auto_gen, prefix) 47 | else 48 | define_attribute(field, mod, auto_gen, binary) 49 | end 50 | define_created_at(field, mod) if created_at 51 | end 52 | end 53 | 54 | # Defines the attribute method that will be written in the module 55 | # 56 | # @api private 57 | # 58 | # @param field [String, Symbol] the name of the field to set as an attribute 59 | # @param mod [Module] the module to extend 60 | # @param auto_gen [Boolean] whether to generate a KSUID upon initialization 61 | # @param binary [Boolean] whether to store the KSUID as a binary or a string 62 | # @return [void] 63 | def self.define_attribute(field, mod, auto_gen, binary) 64 | type = 'ksuid' 65 | type = 'ksuid_binary' if binary 66 | default = 'default: -> { ::KSUID.new }' if auto_gen 67 | 68 | mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 69 | def self.included(base) # def self.included(base) 70 | base.__send__( # base.__send__( 71 | :attribute, # :attribute, 72 | :#{field}, # :id, 73 | :#{type}, # :ksuid, 74 | #{default} # default: -> { ::KSUID.new } 75 | ) # ) 76 | end # end 77 | RUBY 78 | end 79 | private_class_method :define_attribute 80 | 81 | # Defines the attribute method that will be written in the module for a field 82 | # 83 | # @api private 84 | # 85 | # @param field [String, Symbol] the name of the field to set as an attribute 86 | # @param mod [Module] the module to extend 87 | # @param auto_gen [Boolean] whether to generate a KSUID upon initialization 88 | # @param prefix [String] the prefix to add to the KSUID 89 | # @return [void] 90 | def self.define_prefixed_attribute(field, mod, auto_gen, prefix) 91 | default = "default: -> { ::KSUID.prefixed('#{prefix}') }" if auto_gen 92 | 93 | mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 94 | def self.included(base) # def self.included(base) 95 | base.__send__( # base.__send__( 96 | :attribute, # :attribute, 97 | :#{field}, # :id, 98 | :ksuid_prefixed, # :ksuid_prefixed, 99 | prefix: #{prefix.inspect}, # prefix: 'evt_' 100 | #{default} # default: -> { ::KSUID.prefixed('evt_') } 101 | ) # ) 102 | end # end 103 | RUBY 104 | end 105 | private_class_method :define_prefixed_attribute 106 | 107 | # Defines the `#created_at` method that will be written in the module 108 | # 109 | # @api private 110 | # 111 | # @param field [String, Symbol] the name of the KSUID attribute field 112 | # @param mod [Module] the module to extend 113 | # @return [void] 114 | def self.define_created_at(field, mod) 115 | mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 116 | def created_at # def created_at 117 | return unless #{field} # return unless ksuid 118 | 119 | #{field}.to_time # ksuid.to_time 120 | end # end 121 | RUBY 122 | end 123 | private_class_method :define_created_at 124 | end 125 | end 126 | 127 | require 'active_record/ksuid/railtie' if defined?(Rails) 128 | -------------------------------------------------------------------------------- /activerecord-ksuid/lib/active_record/ksuid/binary_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module KSUID 5 | # A binary-serialized KSUID for storage within an ActiveRecord database 6 | # 7 | # @api private 8 | # 9 | # @example Set an attribute as a KSUID using the verbose syntax 10 | # class EventWithBareBinaryType < ActiveRecord::Base 11 | # attribute :ksuid, ActiveRecord::KSUID::BinaryType.new, default: -> { KSUID.new } 12 | # end 13 | # 14 | # @example Set an attribute as a KSUID using the pre-registered type 15 | # class EventWithRegisteredBinaryType < ActiveRecord::Base 16 | # attribute :ksuid, :ksuid_binary, default: -> { KSUID.new } 17 | # end 18 | class BinaryType < ::ActiveRecord::Type::Binary 19 | # Casts a value from user input into a KSUID 20 | # 21 | # Type casting happens via the attribute setter and can take input from 22 | # many places, including: 23 | # 24 | # 1. The Rails form builder 25 | # 2. Directly from the attribute setter 26 | # 3. From the model initializer 27 | # 28 | # @param value [String, Array, KSUID::Type] the value to cast into a KSUID 29 | # @return [KSUID::Type] the type-casted value 30 | def cast(value) 31 | ::KSUID.call(value) 32 | end 33 | 34 | # Converts a value from database input to a KSUID 35 | # 36 | # @param value [String, nil] the database-serialized KSUID to convert 37 | # @return [KSUID::Type] the deserialized KSUID 38 | def deserialize(value) 39 | return unless value 40 | 41 | value = value.to_s if value.is_a?(::ActiveRecord::Type::Binary::Data) 42 | value = ::KSUID::Utils.byte_string_from_hex(value[2..]) if value.start_with?('\x') 43 | ::KSUID.call(value) 44 | end 45 | 46 | # Casts the value from a KSUID into a database-understandable format 47 | # 48 | # @param value [KSUID::Type, nil] the KSUID in Ruby format 49 | # @return [String, nil] the base 62-encoded KSUID for storage in the database 50 | def serialize(value) 51 | return unless value 52 | 53 | super(::KSUID.call(value).to_bytes) 54 | end 55 | end 56 | end 57 | end 58 | 59 | ActiveRecord::Type.register(:ksuid_binary, ActiveRecord::KSUID::BinaryType) 60 | -------------------------------------------------------------------------------- /activerecord-ksuid/lib/active_record/ksuid/prefixed_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module KSUID 5 | # A string-serialized, prefixed KSUID for storage within an ActiveRecord database 6 | # 7 | # @api private 8 | # @since 0.5.0 9 | # 10 | # @example Set an attribute as a prefixed KSUID using the verbose syntax 11 | # class EventWithBarePrefixedType < ActiveRecord::Base 12 | # attribute( 13 | # :ksuid, 14 | # ActiveRecord::KSUID::PrefixedType.new(prefix: 'evt_'), 15 | # default: -> { KSUID.prefixed('evt_') } 16 | # ) 17 | # end 18 | # 19 | # @example Set an attribute as a prefixed KSUID using the pre-registered type 20 | # class EventWithRegisteredPrefixedType < ActiveRecord::Base 21 | # attribute :ksuid, :ksuid_prefixed, prefix: 'evt_', default: -> { KSUID.prefixed('evt_') } 22 | # end 23 | class PrefixedType < ::ActiveRecord::Type::String 24 | # Instantiates an ActiveRecord::Type for handling prefixed KSUIDs 25 | # 26 | # @param prefix [String] the prefix to add to the KSUID 27 | def initialize(prefix: '') 28 | @prefix = prefix 29 | super() 30 | end 31 | 32 | # Casts a value from user input into a {KSUID::Prefixed} 33 | # 34 | # Type casting happens via the attribute setter and can take input from 35 | # many places, including: 36 | # 37 | # 1. The Rails form builder 38 | # 2. Directly from the attribute setter 39 | # 3. From the model initializer 40 | # 41 | # @param value [String, Array, KSUID::Prefixed] the value to cast into a KSUID 42 | # @return [KSUID::Prefixed] the type-casted value 43 | def cast(value) 44 | ::KSUID::Prefixed.call(value, prefix: @prefix) 45 | end 46 | 47 | # Converts a value from database input to a {KSUID::Prefixed} 48 | # 49 | # @param value [String, nil] the database-serialized, prefixed KSUID to convert 50 | # @return [KSUID::Prefixed] the deserialized, prefixed KSUID 51 | def deserialize(value) 52 | return unless value 53 | 54 | ::KSUID::Prefixed.from_base62(value, prefix: @prefix) 55 | end 56 | 57 | # Casts the value from a KSUID into a database-understandable format 58 | # 59 | # @param value [KSUID::Prefixed, nil] the prefixed KSUID in Ruby format 60 | # @return [String, nil] the base 62-encoded, prefixed KSUID for storage in the database 61 | def serialize(value) 62 | return unless value 63 | 64 | ::KSUID::Prefixed.call(value, prefix: @prefix).to_s 65 | end 66 | end 67 | end 68 | end 69 | 70 | ActiveRecord::Type.register(:ksuid_prefixed, ActiveRecord::KSUID::PrefixedType) 71 | -------------------------------------------------------------------------------- /activerecord-ksuid/lib/active_record/ksuid/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module KSUID 5 | # Enables the usage of KSUID types within ActiveRecord when Rails is loaded 6 | # 7 | # @api private 8 | class Railtie < ::Rails::Railtie 9 | initializer 'ksuid' do 10 | require 'ksuid' 11 | 12 | ActiveSupport.on_load :active_record do 13 | require 'active_record/ksuid' 14 | end 15 | end 16 | 17 | initializer 'ksuid.table_definition' do 18 | ActiveSupport.on_load :active_record do 19 | require 'active_record/ksuid/table_definition' 20 | 21 | ActiveRecord::ConnectionAdapters::TableDefinition.include( 22 | ActiveRecord::KSUID::TableDefinition 23 | ) 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /activerecord-ksuid/lib/active_record/ksuid/table_definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module KSUID 5 | # Extends ActiveRecord's table definition language for KSUIDs 6 | module TableDefinition 7 | # Defines a field as a string-based KSUID 8 | # 9 | # @example Define a KSUID field as a non-primary key 10 | # ActiveRecord::Schema.define do 11 | # create_table :events, force: true do |table| 12 | # table.ksuid :ksuid, index: true, unique: true 13 | # end 14 | # end 15 | # 16 | # @example Define a KSUID field as a primary key 17 | # ActiveRecord::Schema.define do 18 | # create_table :events, force: true, id: false do |table| 19 | # table.ksuid :id, primary_key: true 20 | # end 21 | # end 22 | # 23 | # @param args [Array] the list of fields to define as KSUIDs 24 | # @param options [Hash] see {ActiveRecord::ConnectionAdapters::TableDefinition} 25 | # @option options [String] :prefix the prefix expected in front of the KSUID 26 | # @return [void] 27 | def ksuid(*args, **options) 28 | prefix_length = options.delete(:prefix)&.length || 0 29 | 30 | args.each { |name| column(name, :string, **options.merge(limit: 27 + prefix_length)) } 31 | end 32 | 33 | # Defines a field as a binary-based KSUID 34 | # 35 | # @example Define a KSUID field as a non-primary key 36 | # ActiveRecord::Schema.define do 37 | # create_table :events, force: true do |table| 38 | # table.ksuid_binary :ksuid, index: true, unique: true 39 | # end 40 | # end 41 | # 42 | # @example Define a KSUID field as a primary key 43 | # ActiveRecord::Schema.define do 44 | # create_table :events, force: true, id: false do |table| 45 | # table.ksuid_binary :id, primary_key: true 46 | # end 47 | # end 48 | # 49 | # @param args [Array] the list of fields to define as KSUIDs 50 | # @param options [Hash] see {ActiveRecord::ConnectionAdapters::TableDefinition} 51 | # @return [void] 52 | def ksuid_binary(*args, **options) 53 | args.each { |name| column(name, :binary, **options.merge(limit: 20)) } 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /activerecord-ksuid/lib/active_record/ksuid/type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module KSUID 5 | # A string-serialized KSUID for storage within an ActiveRecord database 6 | # 7 | # @api private 8 | # 9 | # @example Set an attribute as a KSUID using the verbose syntax 10 | # class EventWithBareType < ActiveRecord::Base 11 | # attribute :ksuid, ActiveRecord::KSUID::Type.new, default: -> { KSUID.new } 12 | # end 13 | # 14 | # @example Set an attribute as a KSUID using the pre-registered type 15 | # class EventWithRegisteredType < ActiveRecord::Base 16 | # attribute :ksuid, :ksuid, default: -> { KSUID.new } 17 | # end 18 | class Type < ::ActiveRecord::Type::String 19 | # Casts a value from user input into a KSUID 20 | # 21 | # Type casting happens via the attribute setter and can take input from 22 | # many places, including: 23 | # 24 | # 1. The Rails form builder 25 | # 2. Directly from the attribute setter 26 | # 3. From the model initializer 27 | # 28 | # @param value [String, Array, KSUID::Type] the value to cast into a KSUID 29 | # @return [KSUID::Type] the type-casted value 30 | def cast(value) 31 | ::KSUID.call(value) 32 | end 33 | 34 | # Converts a value from database input to a KSUID 35 | # 36 | # @param value [String, nil] the database-serialized KSUID to convert 37 | # @return [KSUID::Type] the deserialized KSUID 38 | def deserialize(value) 39 | return unless value 40 | 41 | ::KSUID.from_base62(value) 42 | end 43 | 44 | # Casts the value from a KSUID into a database-understandable format 45 | # 46 | # @param value [KSUID::Type, nil] the KSUID in Ruby format 47 | # @return [String, nil] the base 62-encoded KSUID for storage in the database 48 | def serialize(value) 49 | return unless value 50 | 51 | ::KSUID.call(value).to_s 52 | end 53 | end 54 | end 55 | end 56 | 57 | ActiveRecord::Type.register(:ksuid, ActiveRecord::KSUID::Type) 58 | -------------------------------------------------------------------------------- /activerecord-ksuid/lib/active_record/ksuid/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module KSUID 5 | # The version of the activerecord-ksuid gem 6 | # 7 | # @return [String] 8 | VERSION = '1.0.0' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /activerecord-ksuid/lib/activerecord-ksuid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_record/ksuid' 4 | -------------------------------------------------------------------------------- /activerecord-ksuid/spec/active_record/ksuid/railtie_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logger' 4 | 5 | begin 6 | require 'rails' 7 | require 'active_record' 8 | rescue LoadError 9 | warn <<~MSG 10 | Skipping Rails tests because you're not running in Appraisals 11 | 12 | Try running `appraisal rspec` or `appraisal rails-7.0 rspec` 13 | MSG 14 | return 15 | end 16 | 17 | require 'active_record/ksuid/railtie' 18 | 19 | ActiveRecord::Base.establish_connection( 20 | adapter: ENV.fetch('DRIVER'), 21 | host: ENV['DB_HOST'], # rubocop:disable Style/FetchEnvVar 22 | username: ENV['DB_USERNAME'], # rubocop:disable Style/FetchEnvVar 23 | database: ENV.fetch('DATABASE', 'activerecord-ksuid_test') 24 | ) 25 | ActiveRecord::Base.logger = Logger.new(IO::NULL) 26 | ActiveRecord::Schema.verbose = false 27 | 28 | # Bootstrap the railtie without booting a Rails app 29 | ActiveRecord::KSUID::Railtie.initializers.each(&:run) 30 | ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) 31 | 32 | ActiveRecord::Schema.define do 33 | create_table :events, force: true do |t| 34 | t.string :ksuid, index: true, unique: true 35 | end 36 | 37 | create_table :event_primary_keys, force: true, id: false do |t| 38 | t.ksuid :id, primary_key: true 39 | end 40 | 41 | create_table :event_binaries, force: true, id: false do |t| 42 | t.ksuid_binary :id, primary_key: true 43 | end 44 | 45 | create_table :event_correlations, force: true do |t| 46 | t.references :from, type: :string, limit: 27 47 | t.references :to, type: :string, limit: 27 48 | end 49 | 50 | create_table :event_binary_correlations, force: true do |t| 51 | t.references :from, type: :binary, limit: 20 52 | t.references :to, type: :binary, limit: 20 53 | end 54 | 55 | create_table :event_prefixes, force: true, id: false do |t| 56 | t.ksuid :id, primary_key: true, prefix: 'evt_' 57 | end 58 | end 59 | 60 | # A demonstration model for testing ActiveRecord::KSUID 61 | class Event < ActiveRecord::Base 62 | include ActiveRecord::KSUID[:ksuid, created_at: true] 63 | end 64 | 65 | # A demonstration of KSUIDs as the primary key on a record 66 | class EventPrimaryKey < ActiveRecord::Base 67 | include ActiveRecord::KSUID[:id] 68 | end 69 | 70 | # A demonstration of KSUIDs persisted as binaries 71 | class EventBinary < ActiveRecord::Base 72 | include ActiveRecord::KSUID[:id, binary: true] 73 | end 74 | 75 | # A demonstration of a relation to a string KSUID primary key 76 | class EventCorrelation < ActiveRecord::Base 77 | include ActiveRecord::KSUID[:from_id, auto_gen: false] 78 | include ActiveRecord::KSUID[:to_id, auto_gen: false] 79 | 80 | belongs_to :from, class_name: 'EventPrimaryKey' 81 | belongs_to :to, class_name: 'EventPrimaryKey' 82 | end 83 | 84 | # A demonstration of a relation to a binary KSUID primary key 85 | class EventBinaryCorrelation < ActiveRecord::Base 86 | include ActiveRecord::KSUID[:from_id, auto_gen: false, binary: true] 87 | include ActiveRecord::KSUID[:to_id, auto_gen: false, binary: true] 88 | 89 | belongs_to :from, class_name: 'EventBinary' 90 | belongs_to :to, class_name: 'EventBinary' 91 | end 92 | 93 | # A demonstration of a prefixed KSUID 94 | class EventPrefix < ActiveRecord::Base 95 | include ActiveRecord::KSUID[:id, prefix: 'evt_'] 96 | end 97 | 98 | RSpec.describe 'ActiveRecord integration', type: :integration do 99 | context 'with a non-primary field as the KSUID' do 100 | after { Event.delete_all } 101 | 102 | it 'generates a KSUID upon initialization' do 103 | event = Event.new 104 | 105 | expect(event.ksuid).to be_a(KSUID::Type) 106 | end 107 | 108 | it 'restores a KSUID from the database' do 109 | ksuid = Event.create!.ksuid 110 | event = Event.last 111 | 112 | expect(event.ksuid).to eq(ksuid) 113 | end 114 | 115 | it 'can be used as a timestamp for the created_at' do 116 | event = Event.create! 117 | 118 | expect(event.created_at).not_to be_nil 119 | end 120 | 121 | it 'can be looked up via a string, byte array, or KSUID', :aggregate_failures do 122 | id = KSUID.new 123 | event = Event.create!(ksuid: id) 124 | 125 | expect(Event.find_by(ksuid: id.to_s)).to eq(event) 126 | expect(Event.find_by(ksuid: id.to_bytes)).to eq(event) 127 | expect(Event.find_by(ksuid: id)).to eq(event) 128 | end 129 | end 130 | 131 | context 'with a primary key field as the KSUID' do 132 | after { EventPrimaryKey.delete_all } 133 | 134 | it 'generates a KSUID upon initialization' do 135 | event = EventPrimaryKey.new 136 | 137 | expect(event.id).to be_a(KSUID::Type) 138 | end 139 | end 140 | 141 | context 'with a binary KSUID field' do 142 | after { EventBinary.delete_all } 143 | 144 | it 'generates a KSUID upon initialization' do 145 | event = EventBinary.new 146 | 147 | expect(event.id).to be_a(KSUID::Type) 148 | end 149 | 150 | it 'persists the KSUID to the database' do 151 | event = EventBinary.create 152 | 153 | expect(event.id).to be_a(KSUID::Type) 154 | end 155 | end 156 | 157 | context 'with a prefixed KSUID field' do 158 | after { EventPrefix.delete_all } 159 | 160 | it 'generates a prefixed KSUID upon initialization' do 161 | event = EventPrefix.new 162 | 163 | expect(event.id).to be_a(KSUID::Prefixed) 164 | end 165 | 166 | it 'persists the prefixed KSUID to the database' do 167 | event = EventPrefix.create 168 | 169 | expect(event.id).to be_a(KSUID::Prefixed) 170 | end 171 | 172 | it 'converts a different prefix into the expected one' do 173 | event = EventPrefix.create(id: 'cus_2DTtbae0N9LqMntLxfKjh7jS9ak') 174 | 175 | expect(event.id.to_s).to eq('evt_2DTtbae0N9LqMntLxfKjh7jS9ak') 176 | end 177 | end 178 | 179 | context 'with a reference to string KSUID-keyed tables' do 180 | after do 181 | EventCorrelation.delete_all 182 | EventPrimaryKey.delete_all 183 | end 184 | 185 | it 'can relate to the other model', :aggregate_failures do 186 | event1 = EventPrimaryKey.create! 187 | event2 = EventPrimaryKey.create! 188 | correlation = EventCorrelation.create!(from: event1, to: event2) 189 | 190 | correlation.reload 191 | 192 | expect(correlation.from).to eq event1 193 | expect(correlation.to).to eq event2 194 | end 195 | 196 | it 'can preload the other model', :aggregate_failures do 197 | event1 = EventPrimaryKey.create! 198 | event2 = EventPrimaryKey.create! 199 | 200 | 5.times { EventCorrelation.create!(from: event1, to: event2) } 201 | 202 | expect do 203 | EventCorrelation 204 | .all 205 | .map { |correlation| "#{correlation.from.id} #{correlation.to.id}" } 206 | end.to issue_sql_queries(11) 207 | 208 | expect do 209 | EventCorrelation 210 | .includes(:from, :to) 211 | .map { |correlation| "#{correlation.from.id} #{correlation.to.id}" } 212 | end.to issue_sql_queries(3) 213 | end 214 | 215 | it 'does not initialize fields marked with auto_gen: false', :aggregate_failures do 216 | event = EventCorrelation.new 217 | 218 | expect(event.from_id).to be_nil 219 | expect(event.to_id).to be_nil 220 | end 221 | end 222 | 223 | context 'with a reference to binary KSUID-keyed tables' do 224 | after do 225 | EventBinaryCorrelation.delete_all 226 | EventBinary.delete_all 227 | end 228 | 229 | it 'can relate to the other model', :aggregate_failures do 230 | event1 = EventBinary.create! 231 | event2 = EventBinary.create! 232 | correlation = EventBinaryCorrelation.create!(from: event1, to: event2) 233 | 234 | correlation.reload 235 | 236 | expect(correlation.from).to eq event1 237 | expect(correlation.to).to eq event2 238 | end 239 | 240 | it 'can preload the other model', :aggregate_failures do 241 | event1 = EventBinary.create! 242 | event2 = EventBinary.create! 243 | 244 | 5.times { EventBinaryCorrelation.create!(from: event1, to: event2) } 245 | 246 | expect do 247 | EventBinaryCorrelation 248 | .all 249 | .map { |correlation| "#{correlation.from.id} #{correlation.to.id}" } 250 | end.to issue_sql_queries(11) 251 | 252 | expect do 253 | EventBinaryCorrelation 254 | .includes(:from, :to) 255 | .map { |correlation| "#{correlation.from.id} #{correlation.to.id}" } 256 | end.to issue_sql_queries(3) 257 | end 258 | end 259 | 260 | matcher :issue_sql_queries do |expected| 261 | supports_block_expectations 262 | 263 | match do |actual| 264 | @issued_queries = 0 265 | counter = ->(*) { @issued_queries += 1 } 266 | 267 | ActiveSupport::Notifications.subscribed(counter, 'sql.active_record', &actual) 268 | 269 | expected == @issued_queries 270 | end 271 | 272 | failure_message do 273 | "expected #{expected} queries, issued #{@issued_queries}" 274 | end 275 | end 276 | end 277 | -------------------------------------------------------------------------------- /activerecord-ksuid/spec/doctest_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rails' 4 | require 'active_record' 5 | require 'logger' 6 | 7 | require 'active_record/ksuid/railtie' 8 | 9 | ActiveRecord::KSUID::Railtie.initializers.each(&:run) 10 | ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) 11 | 12 | ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') 13 | ActiveRecord::Base.logger = Logger.new(IO::NULL) 14 | ActiveRecord::Schema.verbose = false 15 | 16 | ActiveSupport::Deprecation.instance.silenced = true 17 | -------------------------------------------------------------------------------- /activerecord-ksuid/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV['COVERAGE'] || ENV['CI'] 4 | require 'simplecov' 5 | 6 | SimpleCov.start do 7 | add_filter '/spec/' 8 | end 9 | end 10 | 11 | begin 12 | require 'pry' 13 | rescue LoadError # rubocop:disable Lint/SuppressedException 14 | end 15 | 16 | require 'active_record' 17 | require 'ksuid' 18 | require 'activerecord-ksuid' 19 | 20 | RSpec.configure do |config| 21 | config.expect_with :rspec do |expectations| 22 | expectations.syntax = :expect 23 | end 24 | 25 | config.mock_with :rspec do |mocks| 26 | mocks.verify_partial_doubles = true 27 | end 28 | 29 | config.disable_monkey_patching! 30 | config.example_status_persistence_file_path = 'spec/examples.txt' 31 | config.filter_run_when_matching :focus 32 | config.shared_context_metadata_behavior = :apply_to_host_groups 33 | config.warnings = true 34 | 35 | config.default_formatter = 'doc' if config.files_to_run.one? 36 | config.profile_examples = 10 if ENV['PROFILE'] 37 | 38 | config.order = :random 39 | Kernel.srand config.seed 40 | end 41 | -------------------------------------------------------------------------------- /ksuid/.reek.yml: -------------------------------------------------------------------------------- 1 | --- 2 | detectors: 3 | ManualDispatch: 4 | exclude: 5 | - "KSUID::Configuration#assert_generator_is_callable" 6 | 7 | UncommunicativeModuleName: 8 | exclude: 9 | - "KSUID::Base62" 10 | 11 | UncommunicativeMethodName: 12 | exclude: 13 | - "KSUID#self.from_base62" 14 | 15 | exclude_paths: 16 | - benchmark/ 17 | 18 | # vim: ft=yaml 19 | -------------------------------------------------------------------------------- /ksuid/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /ksuid/.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | --protected 3 | --markup markdown 4 | --plugin yard-doctest 5 | - 6 | CHANGELOG.md 7 | CONTRIBUTING.md 8 | LICENSE.md 9 | README.md 10 | -------------------------------------------------------------------------------- /ksuid/.yardstick.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 100 3 | -------------------------------------------------------------------------------- /ksuid/CHANGELOG.md: -------------------------------------------------------------------------------- 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 | ## [1.0.0](https://github.com/michaelherold/ksuid/compare/v0.5.0...v1.0.0) - 2023-02-25 8 | 9 | ### Removed 10 | 11 | - Extracted the ActiveRecord functionality into its own gem, `activerecord-ksuid`. It is API-compatible with v0.5.0 so to restore functionality, you should only need to add the new gem to your application. See [the upgrading notice](./UPGRADING.md) for more information. 12 | 13 | ## [0.5.0](https://github.com/michaelherold/ksuid/compare/v0.4.0...v0.5.0) - 2022-08-18 14 | 15 | ### Added 16 | 17 | - If you'd rather deal in KSUID strings instead of `KSUID::Type`s, you can now generate them simply with `KSUID.string`. It takes the same arguments, `payload` and `time` as `KSUID.new`, but returns a string instead of a `KSUID::Type`. 18 | - `KSUID.prefixed` and the `KSUID::Prefixed` class now can generate prefixed KSUIDs to make them visually identifiable for their source. You cannot prefix a binary-encoded KSUID, only base 62-encoded ones. 19 | - `ActiveRecord::KSUID` now accepts a `prefix:` argument for handling prefixed KSUIDs. In addition, the `ksuid` column type also accepts a `prefix:` argument to calculate the intended size of the column with the prefix. 20 | 21 | ### Deprecated 22 | 23 | - `KSUID::ActiveRecord` is now `ActiveRecord::KSUID`. The original constant will continue to work until v1.0.0, but will emit a warning upon boot of your application. To silence the deprecation, change all uses of `KSUID::ActiveRecord` to the new constant, `ActiveRecord::KSUID`. See the [upgrading notice][./UPGRADING.md] for more information. 24 | 25 | ### Miscellaneous 26 | 27 | - The compatibility check for the Base62 implementation in the gem is about 10x faster now. The original optimization did not optimize as much due to an error with the benchmark. This change has a tested benchmark that shows a great improvement. Note that this is a micro-optimization and we see no real performance gain in the parsing of KSUID strings. 28 | 29 | ## [0.4.0](https://github.com/michaelherold/ksuid/compare/v0.3.0...v0.4.0) - 2022-07-29 30 | 31 | ### Added 32 | 33 | - `KSUID::Type` acts as a proper value object now, which means that you may use it as a Hash key or use it in ActiveRecord's `.includes`. `KSUID::Type#eql?`, `KSUID::Type#hash`, and `KSUID::Type#==` now work as expected. Note that `KSUID::Type#==` is more lax than `KSUID::Type#eql?` because it can also match any object that converts to a string matching its value. This means that you can use it to match against `String` KSUIDs. 34 | 35 | ### Fixed 36 | 37 | - `ActiveRecord::QueryMethods#include` works as expected now due to the fix on the value object semantics of `KSUID::Type`. 38 | - Binary KSUID primary and foreign keys work as expected on JRuby. 39 | 40 | ## [0.3.0](https://github.com/michaelherold/ksuid/compare/v0.2.0...v0.3.0) - 2021-10-07 41 | 42 | ### Added 43 | 44 | - A utility function for converting from a hexidecimal-encoded string to a byte string. This is necessary to handle the default encoding of binary fields within PostgreSQL. 45 | 46 | ## [0.2.0](https://github.com/michaelherold/ksuid/compare/v0.1.0...v0.2.0) - 2020-11-11 47 | 48 | ### Added 49 | 50 | - The ability to configure the random generator for the gem via `KSUID.configure`. This allows you to set up random generation to the specifications you need, whether that is for speed or for security. 51 | - Support for ActiveRecord. You can now use `KSUID::ActiveRecord[:my_field]` to define a KSUID field using the Rails 5 Attributes API. There is also two new column types for migrations: `ksuid` and `ksuid_binary`. The first stores your KSUID as a string in the database, the latter as binary data. 52 | 53 | ### Changed 54 | 55 | - The `KSUID::Type#inspect` method now makes it much easier to see what you're looking at in the console when you're debugging. 56 | 57 | ## [0.1.0](https://github.com/michaelherold/ksuid/tree/v0.1.0) - 2017-11-05 58 | 59 | ### Added 60 | 61 | - Basic `KSUID.new` interface. 62 | - Parsing of bytes through `KSUID.from_bytes`. 63 | - Parsing of strings through `KSUID.from_base62`. 64 | -------------------------------------------------------------------------------- /ksuid/CONTRIBUTING.md: -------------------------------------------------------------------------------- 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. Commit and push your changes. 37 | 10. [Submit a pull request]. 38 | 39 | [Create a topic branch]: https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/ 40 | [Fork the repository]: http://learn.github.com/p/branching.html 41 | [Submit a pull request]: https://help.github.com/articles/creating-a-pull-request/ 42 | 43 | ## Tools to Help You Succeed 44 | 45 | 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. 46 | 47 | 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`. 48 | 49 | 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. 50 | 51 | [guard]: http://guardgem.org 52 | [rubygems]: https://rubygems.org 53 | -------------------------------------------------------------------------------- /ksuid/Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | guard :bundler do 4 | watch('Gemfile') 5 | end 6 | 7 | guard :inch do 8 | watch(%r{lib/.+\.rb}) 9 | end 10 | 11 | guard :rspec, cmd: 'bundle exec rspec' do 12 | require 'guard/rspec/dsl' 13 | dsl = Guard::RSpec::Dsl.new(self) 14 | 15 | rspec = dsl.rspec 16 | watch(rspec.spec_helper) { rspec.spec_dir } 17 | watch(rspec.spec_support) { rspec.spec_dir } 18 | watch(rspec.spec_files) 19 | 20 | ruby = dsl.ruby 21 | dsl.watch_spec_files_for(ruby.lib_files) 22 | end 23 | 24 | guard :rubocop do 25 | watch('Rakefile') 26 | watch(/.+\.rb$/) 27 | watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) } 28 | end 29 | 30 | guard :yard do 31 | watch(%r{lib/.+\.rb}) 32 | end 33 | -------------------------------------------------------------------------------- /ksuid/LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright © 2017-2022 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. 21 | -------------------------------------------------------------------------------- /ksuid/README.md: -------------------------------------------------------------------------------- 1 | # KSUID for Ruby 2 | 3 | [![Build Status](https://github.com/michaelherold/ksuid-ruby/workflows/Continuous%20integration/badge.svg)][actions] 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 | [actions]: https://github.com/michaelherold/ksuid-ruby/actions 9 | [inch]: http://inch-ci.org/github/michaelherold/ksuid-ruby 10 | [maintainability]: https://codeclimate.com/github/michaelherold/ksuid-ruby/maintainability 11 | [test-coverage]: https://codeclimate.com/github/michaelherold/ksuid-ruby/test_coverage 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 | If you need to use a faster or more secure way of generating the random payloads (or if you want the payload to be non-random data), you can configure the gem for those use cases: 96 | 97 | ```ruby 98 | KSUID.configure do |config| 99 | config.random_generator = -> { Random.new.bytes(16) } 100 | end 101 | ``` 102 | 103 | ### Prefixed KSUIDs 104 | 105 | If you use KSUIDs in multiple contexts, you can prefix them to make them easily identifiable. 106 | 107 | ```ruby 108 | ksuid = KSUID.prefixed('evt_') 109 | ``` 110 | 111 | Just like a normal KSUID, you can use a specific timestamp: 112 | 113 | ``` ruby 114 | ksuid = KSUID.prefixed('evt_', time: time) # where time is a Time-like object 115 | ``` 116 | 117 | You can also parse a prefixed KSUID from a string that you received: 118 | 119 | ```ruby 120 | ksuid = KSUID::Prefixed.from_base62(base62_string, prefix: 'evt_') 121 | ``` 122 | 123 | Prefixed KSUIDs order themselves with non-prefixed KSUIDs as if their prefix did not exist. With other prefixed KSUIDs, they order first by their prefix, then their timestamp. 124 | 125 | ### Integrations 126 | 127 | KSUID for Ruby can integrate with other systems through adapter gems. Below is a sample of these adapter gems. 128 | 129 | #### ActiveRecord 130 | 131 | If you want to include KSUID columns in your ActiveRecord models, install the `activerecord-ksuid` gem. If you are using it within a Rails app, run the following: 132 | 133 | bundle add activerecord-ksuid --require active_record/ksuid/railtie 134 | 135 | If you are using it outside of Rails, add this to your Gemfile: 136 | 137 | gem 'activerecord-ksuid', require: ['ksuid', 'active_record/ksuid', 'active_record/ksuid/table_definition'] 138 | 139 | See [the readme for the integration](https://github.com/michaelherold/ksuid-ruby/blob/main/activerecord-ksuid/README.md) for more information. 140 | 141 | ## Contributing 142 | 143 | So you’re interested in contributing to KSUID? Check out our [contributing guidelines](CONTRIBUTING.md) for more information on how to do that. 144 | 145 | ## Supported Ruby Versions 146 | 147 | This library aims to support and is [tested against][actions] the following Ruby versions: 148 | 149 | * Ruby 2.7 150 | * Ruby 3.0 151 | * Ruby 3.1 152 | * JRuby 9.3 153 | 154 | If something doesn't work on one of these versions, it's a bug. 155 | 156 | This library may inadvertently work (or seem to work) on other Ruby versions, however support will only be provided for the versions listed above. 157 | 158 | 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. 159 | 160 | ## Versioning 161 | 162 | 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: 163 | 164 | spec.add_dependency "ksuid", "~> 0.1" 165 | 166 | [pessimistic]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint 167 | [semver]: http://semver.org/spec/v2.0.0.html 168 | 169 | ## License 170 | 171 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 172 | -------------------------------------------------------------------------------- /ksuid/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | 5 | # Defines a Rake task if the optional dependency is installed 6 | # 7 | # @return [nil] 8 | def with_optional_dependency 9 | yield if block_given? 10 | rescue LoadError # rubocop:disable Lint/SuppressedException 11 | end 12 | 13 | require 'rspec/core/rake_task' 14 | RSpec::Core::RakeTask.new(:spec) 15 | 16 | default = %w[spec] 17 | 18 | with_optional_dependency do 19 | require 'yard-doctest' 20 | desc 'Run tests on the examples in documentation strings' 21 | task 'yard:doctest' do 22 | command = String.new('yard doctest') 23 | env = {} 24 | success = system(env, command) 25 | 26 | abort "\nYard Doctest failed: #{$CHILD_STATUS}" unless success 27 | end 28 | 29 | default << 'yard:doctest' 30 | end 31 | 32 | with_optional_dependency do 33 | require 'rubocop/rake_task' 34 | RuboCop::RakeTask.new(:rubocop) 35 | 36 | default << 'rubocop' 37 | end 38 | 39 | with_optional_dependency do 40 | require 'yard/rake/yardoc_task' 41 | YARD::Rake::YardocTask.new(:yard) 42 | 43 | default << 'yard' 44 | end 45 | 46 | with_optional_dependency do 47 | require 'inch/rake' 48 | Inch::Rake::Suggest.new(:inch) 49 | 50 | default << 'inch' 51 | end 52 | 53 | with_optional_dependency do 54 | require 'yardstick/rake/measurement' 55 | options = YAML.load_file('.yardstick.yml') 56 | Yardstick::Rake::Measurement.new(:yardstick_measure, options) do |measurement| 57 | measurement.output = 'coverage/docs.txt' 58 | end 59 | 60 | require 'yardstick/rake/verify' 61 | options = YAML.load_file('.yardstick.yml') 62 | Yardstick::Rake::Verify.new(:yardstick_verify, options) do |verify| 63 | verify.threshold = 100 64 | end 65 | 66 | task yardstick: %i[yardstick_measure yardstick_verify] 67 | end 68 | 69 | task default: default 70 | -------------------------------------------------------------------------------- /ksuid/UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading instructions for KSUID for Ruby 2 | 3 | ## v1.0.0 4 | 5 | ### Extracted `ActiveRecord::KSUID` into its own gem 6 | 7 | That KSUID for Ruby included ActiveRecord support directly in its gem has always been a regret of mine. It adds ActiveRecord and Rails concerns to a gem that you can use in any context. It makes running the test suite more complicated for no real gain. And it makes it kludgy to add support for more systems, like Sequel, since you have conflicting concerns in the same gem. 8 | 9 | To remove this problem, v1.0.0 extracts the ActiveRecord behavior into its own gem, `activerecord-ksuid`. This version is a straight extraction with an improved test suite so it _should_ mean that the only change you have to make when upgrading from v0.5.0 is to do the following in your Gemfile: 10 | 11 | ```diff 12 | - gem 'ksuid' 13 | + gem 'activerecord-ksuid' 14 | ``` 15 | 16 | If you are still on a version prior to v0.5.0, upgrade to that version first, solve the deprecation notice below, ensure your app still works, and then upgrade to v1.0.0. 17 | 18 | ## v0.5.0 19 | 20 | ### Deprecated `KSUID::ActiveRecord` in favor of `ActiveRecord::KSUID` 21 | 22 | This version deprecates the original constant for the ActiveRecord integration, `KSUID::ActiveRecord`. This change is in preparation for extracting the ActiveRecord integration into its own gem. Continuing to use the original constant will show deprecation warnings upon boot of your application. 23 | 24 | Migrating for this version should be quick: simply do a global replace of `KSUID::ActiveRecord` for `ActiveRecord::KSUID`. No other changes should be necessary. 25 | 26 | In the future release of v1.0.0, you will need to also include `activerecord-ksuid` your Gemfile. This gem is as-yet unreleased, with a release intended concurrently with v1.0.0 of KSUID for Ruby. 27 | -------------------------------------------------------------------------------- /ksuid/benchmark/changes.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # A benchmark that tests the performance of changes in the library 5 | # 6 | # It uses the "hold" system of benchmark-ips, which means that you 7 | # run it once as a baseline, make changes, then run it again to compare 8 | 9 | $LOAD_PATH.unshift File.expand_path(__dir__, "../lib") 10 | 11 | require 'benchmark/ips' 12 | require 'ksuid' 13 | 14 | EXAMPLE = '15Ew2nYeRDscBipuJicYjl970D1' 15 | BINARY_EXAMPLE = ("\xFF" * 20).b 16 | 17 | Benchmark.ips do |bench| 18 | bench.report('baseline') { KSUID::Base62.compatible?(EXAMPLE) } 19 | bench.report('experiment') { KSUID::Base62.compatible?(EXAMPLE) } 20 | 21 | bench.hold!('changes.jsonld') 22 | bench.compare! 23 | end 24 | -------------------------------------------------------------------------------- /ksuid/benchmark/charset_inclusion.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A benchmark that tests different ways to ensure a string matches 4 | # only characters from the KSUID Base62 charset. 5 | 6 | require 'benchmark/ips' 7 | require 'set' 8 | 9 | EXAMPLE = '15Ew2nYeRDscBipuJicYjl970D1' 10 | BAD_EXAMPLE = '15Ew2nYeRDscBipuJicYjl970D!' 11 | CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 12 | MATCHER = /[^#{CHARSET}]/ 13 | CHARS = CHARSET.chars 14 | SET = Set.new(CHARS) 15 | 16 | # Checks each char individually against the charset 17 | def include?(example) 18 | example.each_char.all? { |c| CHARSET.include?(c) } 19 | end 20 | 21 | # Checks for a match against anything not in the charset 22 | def regexp_match?(example) 23 | !MATCHER.match?(example) 24 | end 25 | 26 | # Checks each character against the regexp 27 | def match_by_char?(example) 28 | example.each_char.all? { |c| !MATCHER.match?(c) } 29 | end 30 | 31 | # Splits the string and uses array difference to check for stray characters 32 | def split_diff(example) 33 | (example.split('') - CHARS).empty? 34 | end 35 | 36 | # Uses a two-finger method to check each character against each charset member 37 | # exactly once 38 | def two_finger(example) 39 | example = example.chars.sort 40 | left, right = [0, 0] 41 | 42 | while left < example.length && right < CHARSET.length do 43 | if example[left] == CHARSET[right] 44 | left += 1 45 | else 46 | right += 1 47 | end 48 | end 49 | 50 | left == example.length && 51 | example[left - 1] == CHARSET[right] 52 | end 53 | 54 | def set_include?(example) 55 | example.each_char.all? { |c| SET.include?(c) } 56 | end 57 | 58 | include?(EXAMPLE) or raise "include? does not work" 59 | regexp_match?(EXAMPLE) or raise "regexp_match? does not work" 60 | match_by_char?(EXAMPLE) or raise "match_by_char? does not work" 61 | split_diff(EXAMPLE) or raise "split_diff does not work" 62 | two_finger(EXAMPLE) or raise "two_finger does not work" 63 | set_include?(EXAMPLE) or raise "set_include? does not work" 64 | 65 | include?(BAD_EXAMPLE) and raise "include? does not work" 66 | regexp_match?(BAD_EXAMPLE) and raise "regexp_match? does not work" 67 | match_by_char?(BAD_EXAMPLE) and raise "match_by_char? does not work" 68 | split_diff(BAD_EXAMPLE) and raise "split_diff does not work" 69 | two_finger(BAD_EXAMPLE) and raise "two_finger does not work" 70 | set_include?(BAD_EXAMPLE) and raise "set_include? does not work" 71 | 72 | puts "Benchmarking good example\n\n" 73 | 74 | Benchmark.ips do |bench| 75 | bench.report('include?') { include?(EXAMPLE) } 76 | bench.report('Regexp#match?') { regexp_match?(EXAMPLE) } 77 | bench.report('#match?(char)') { match_by_char?(EXAMPLE) } 78 | bench.report('Array#-') { split_diff(EXAMPLE) } 79 | bench.report('two finger') { two_finger(EXAMPLE) } 80 | bench.report('Set#include?') { set_include?(EXAMPLE) } 81 | 82 | bench.compare! 83 | end 84 | 85 | puts "Benchmarking bad example\n\n" 86 | 87 | Benchmark.ips do |bench| 88 | bench.report('include?') { include?(BAD_EXAMPLE) } 89 | bench.report('Regexp#match?') { regexp_match?(BAD_EXAMPLE) } 90 | bench.report('#match?(char)') { match_by_char?(BAD_EXAMPLE) } 91 | bench.report('Array#-') { split_diff(BAD_EXAMPLE) } 92 | bench.report('two finger') { two_finger(BAD_EXAMPLE) } 93 | bench.report('Set#include?') { set_include?(BAD_EXAMPLE) } 94 | 95 | bench.compare! 96 | end 97 | -------------------------------------------------------------------------------- /ksuid/benchmark/random_generators.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'benchmark/ips' 5 | require 'json' 6 | require 'ksuid' 7 | 8 | RESULTS_FILE = '.random_generator.json' 9 | NON_RANDOM_DATA = "\x00" * 16 10 | 11 | 3.times do 12 | if File.exist?(RESULTS_FILE) 13 | results = File.readlines(RESULTS_FILE).map { |line| JSON.parse(line)['item'] } 14 | 15 | if !results.include?('random') 16 | generator = Random.new 17 | KSUID.configure { |config| config.random_generator = -> { generator.bytes(16) } } 18 | else 19 | KSUID.configure { |config| config.random_generator = -> { NON_RANDOM_DATA } } 20 | end 21 | end 22 | 23 | Benchmark.ips do |x| 24 | x.report('securerandom') { KSUID.new } 25 | x.report('random') { KSUID.new } 26 | x.report('non-random') { KSUID.new } 27 | 28 | x.compare! 29 | x.hold!(RESULTS_FILE) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /ksuid/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'ksuid' 6 | require 'irb' 7 | 8 | IRB.start(__FILE__) 9 | -------------------------------------------------------------------------------- /ksuid/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | set -vx 6 | 7 | bundle install 8 | -------------------------------------------------------------------------------- /ksuid/checksums/ksuid-1.0.0.gem.sha512: -------------------------------------------------------------------------------- 1 | 04124c74bd5224635c64809a088482415964d79cb39aba1af077145a5e77f0914799faff2e1f7b4df94e629bbeb122aa622160eae917478c995ceea9a7d94903 2 | -------------------------------------------------------------------------------- /ksuid/ksuid.gemspec: -------------------------------------------------------------------------------- 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 = ['opensource@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-ruby' 14 | spec.license = 'MIT' 15 | 16 | spec.files = %w[CHANGELOG.md CONTRIBUTING.md LICENSE.md README.md UPGRADING.md] 17 | spec.files += %w[ksuid.gemspec] 18 | spec.files += Dir['lib/**/*.rb'] 19 | spec.require_paths = ['lib'] 20 | 21 | spec.metadata['rubygems_mfa_required'] = 'true' 22 | 23 | spec.add_development_dependency 'bundler', '>= 1.15' 24 | end 25 | -------------------------------------------------------------------------------- /ksuid/lib/ksuid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'ksuid/configuration' 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 Generate a KSUID prefixed by `evt_` 44 | # KSUID.prefixed('evt_') 45 | # 46 | # @example Parse a KSUID string that you have received 47 | # KSUID.from_base62('aWgEPTl1tmebfsQzFP4bxwgy80V') 48 | # 49 | # @example Parse a KSUID byte string that you have received 50 | # KSUID.from_bytes( 51 | # "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" 52 | # ) 53 | # 54 | # @example Parse a KSUID byte array that you have received 55 | # KSUID.from_bytes( 56 | # [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 57 | # 255, 255, 255, 255] 58 | # ) 59 | module KSUID 60 | # The shift in the Unix epoch time between the standard and the KSUID base 61 | # 62 | # @return [Integer] the number of seconds by which we shift the epoch 63 | EPOCH_TIME = 1_400_000_000 64 | 65 | # The number of bytes that are used to represent each part of a KSUID 66 | # 67 | # @return [Hash{Symbol => Integer}] the map of data type to number of bytes 68 | BYTES = { base62: 27, payload: 16, timestamp: 4, total: 20 }.freeze 69 | 70 | # The number of characters in a base 62-encoded KSUID 71 | # 72 | # @return [Integer] 73 | STRING_LENGTH = 27 74 | 75 | # The maximum KSUID as a base 62-encoded string. 76 | # 77 | # @return [String] 78 | MAX_STRING_ENCODED = 'aWgEPTl1tmebfsQzFP4bxwgy80V' 79 | 80 | autoload :Base62, 'ksuid/base62' 81 | autoload :Prefixed, 'ksuid/prefixed' 82 | autoload :Type, 'ksuid/type' 83 | autoload :Utils, 'ksuid/utils' 84 | 85 | # Converts a KSUID-compatible value into an actual KSUID 86 | # 87 | # @api public 88 | # 89 | # @example Converts a base 62 KSUID string into a KSUID 90 | # KSUID.call('15Ew2nYeRDscBipuJicYjl970D1') 91 | # 92 | # @param ksuid [String, Array, KSUID::Type] the KSUID-compatible value 93 | # @return [KSUID::Type] the converted KSUID 94 | # @raise [ArgumentError] if the value is not KSUID-compatible 95 | def self.call(ksuid) 96 | return unless ksuid 97 | 98 | case ksuid 99 | when KSUID::Prefixed then ksuid.to_ksuid 100 | when KSUID::Type then ksuid 101 | when Array then KSUID.from_bytes(ksuid) 102 | when String then cast_string(ksuid) 103 | else 104 | raise ArgumentError, "Cannot convert #{ksuid.inspect} to KSUID" 105 | end 106 | end 107 | 108 | # The configuration for creating new KSUIDs 109 | # 110 | # @api private 111 | # 112 | # @return [KSUID::Configuration] the gem's configuration 113 | def self.config 114 | @config ||= KSUID::Configuration.new 115 | end 116 | 117 | # Configures the KSUID gem by passing a block 118 | # 119 | # @api public 120 | # 121 | # @example Override the random generator with a null data generator 122 | # KSUID.configure do |config| 123 | # config.random_generator = -> { "\x00" * KSUID::BYTES[:payload] } 124 | # end 125 | # 126 | # @example Override the random generator with the faster, but less secure, Random 127 | # KSUID.configure do |config| 128 | # config.random_generator = -> { Random.new.bytes(KSUID::BYTES[:payload]) } 129 | # end 130 | # 131 | # @return [KSUID::Configuration] the gem's configuration 132 | def self.configure 133 | yield config if block_given? 134 | config 135 | end 136 | 137 | # Converts a base 62-encoded string into a KSUID 138 | # 139 | # @api public 140 | # 141 | # @example Parse a KSUID string into an object 142 | # KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW') 143 | # 144 | # @param string [String] the base 62-encoded KSUID to convert into an object 145 | # @return [KSUID::Type] the KSUID generated from the string 146 | def self.from_base62(string) 147 | string = string.rjust(STRING_LENGTH, Base62::CHARSET[0]) if string.length < STRING_LENGTH 148 | int = Base62.decode(string) 149 | bytes = Utils.int_to_bytes(int, 160) 150 | 151 | from_bytes(bytes) 152 | end 153 | 154 | # Converts a byte string or byte array into a KSUID 155 | # 156 | # @api public 157 | # 158 | # @example Parse a KSUID byte string into an object 159 | # KSUID.from_bytes("\x06\x83\xF7\x89\x04\x9C\xC2\x15\xC0\x99\xD4+xM\xBE\x994\e\xD7\x9C") 160 | # 161 | # @param bytes [String, Array] the byte string or array to convert into an object 162 | # @return [KSUID::Type] the KSUID generated from the bytes 163 | def self.from_bytes(bytes) 164 | bytes = bytes.bytes if bytes.is_a?(String) 165 | 166 | timestamp = Utils.int_from_bytes(bytes.first(BYTES[:timestamp])) 167 | payload = Utils.byte_string_from_array(bytes.last(BYTES[:payload])) 168 | 169 | KSUID::Type.new(payload: payload, time: Time.at(timestamp + EPOCH_TIME)) 170 | end 171 | 172 | # Generates the maximum KSUID as a KSUID type 173 | # 174 | # @api semipublic 175 | # 176 | # @example Generate the maximum KSUID 177 | # KSUID.max 178 | # 179 | # @return [KSUID::Type] the maximum KSUID in both timestamp and payload 180 | def self.max 181 | from_bytes([255] * BYTES[:total]) 182 | end 183 | 184 | # Instantiates a new KSUID 185 | # 186 | # @api public 187 | # 188 | # @example Generate a new KSUID for the current second 189 | # KSUID.new 190 | # 191 | # @example Generate a new KSUID for a given timestamp 192 | # KSUID.new(time: Time.parse('2017-11-05 15:00:04 UTC')) 193 | # 194 | # @param payload [String, Array, nil] the payload for the KSUID 195 | # @param time [Time] the timestamp to use for the KSUID 196 | # @return [KSUID::Type] the generated KSUID 197 | def self.new(payload: nil, time: Time.now) 198 | Type.new(payload: payload, time: time) 199 | end 200 | 201 | # Instantiates a new {KSUID::Prefixed} 202 | # 203 | # @api public 204 | # @since 0.5.0 205 | # 206 | # @example Generate a new prefixed KSUID for the current second 207 | # KSUID.prefixed('evt_') 208 | # 209 | # @example Generate a new prefixed KSUID for a given timestamp 210 | # KSUID.prefixed('cus_', time: Time.parse('2022-08-16 10:36:00 UTC')) 211 | # 212 | # @param prefix [String] the prefix to apply to the KSUID 213 | # @param payload [String, Array, nil] the payload for the KSUID 214 | # @param time [Time] the timestamp to use for the KSUID 215 | # @return [KSUID::Prefixed] the generated, prefixed KSUID 216 | def self.prefixed(prefix, payload: nil, time: Time.now) 217 | Prefixed.new(prefix, payload: payload, time: time) 218 | end 219 | 220 | # Generates a KSUID string 221 | # 222 | # @api public 223 | # @since 0.5.0 224 | # 225 | # @example Generate a new KSUID string for the current second 226 | # KSUID.string 227 | # 228 | # @example Generate a new KSUID string for a given timestamp 229 | # KSUID.string(time: Time.parse('2017-11-05 15:00:04 UTC')) 230 | # 231 | # @param payload [String, Array, nil] the payload for the KSUID string 232 | # @param time [Time] the timestamp to use for the KSUID string 233 | # @return [String] the generated string 234 | def self.string(payload: nil, time: Time.now) 235 | Type.new(payload: payload, time: time).to_s 236 | end 237 | 238 | # Casts a string into a KSUID 239 | # 240 | # @api private 241 | # 242 | # @param ksuid [String] the string to convert into a KSUID 243 | # @return [KSUID::Type] the converted KSUID 244 | def self.cast_string(ksuid) 245 | if Base62.compatible?(ksuid) 246 | KSUID.from_base62(ksuid) 247 | else 248 | KSUID.from_bytes(ksuid) 249 | end 250 | end 251 | private_class_method :cast_string 252 | end 253 | -------------------------------------------------------------------------------- /ksuid/lib/ksuid/base62.rb: -------------------------------------------------------------------------------- 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 | # A matcher that checks whether a String has a character outside the charset 24 | # 25 | # @api private 26 | MATCHER = /[^#{CHARSET}]/.freeze 27 | 28 | # Checks whether a string is a base 62-compatible string 29 | # 30 | # @api public 31 | # 32 | # @example Checks a KSUID for base 62 compatibility 33 | # KSUID::Base62.compatible?("15Ew2nYeRDscBipuJicYjl970D1") #=> true 34 | # 35 | # @param string [String] the string to check for compatibility 36 | # @return [Boolean] 37 | def self.compatible?(string) 38 | !MATCHER.match?(string) 39 | end 40 | 41 | # Decodes a base 62-encoded string into an integer 42 | # 43 | # @api public 44 | # 45 | # @example Decode a string into a number 46 | # KSUID::Base62.decode('0000000000000000000001LY7VK') 47 | # #=> 1234567890 48 | # 49 | # @param ksuid [String] the base 62-encoded number 50 | # @return [Integer] the decoded number as an integer 51 | def self.decode(ksuid) 52 | result = 0 53 | 54 | ksuid.chars.each_with_index do |char, position| 55 | unless (digit = CHARSET.index(char)) 56 | raise(ArgumentError, "#{ksuid} is not a base 62 number") 57 | end 58 | 59 | result += digit * (BASE**(ksuid.length - (position + 1))) 60 | end 61 | 62 | result 63 | end 64 | 65 | # Encodes a number (integer) as a base 62 string 66 | # 67 | # @api public 68 | # 69 | # @example Encode a number as a base 62 string 70 | # KSUID::Base62.encode(1_234_567_890) 71 | # #=> "0000000000000000000001LY7VK" 72 | # 73 | # @param number [Integer] the number to encode into base 62 74 | # @return [String] the base 62-encoded number 75 | def self.encode(number) 76 | chars = encode_without_padding(number) 77 | 78 | chars << padding if chars.empty? 79 | chars.reverse.join.rjust(STRING_LENGTH, padding) 80 | end 81 | 82 | # Encodes a byte string or byte array into base 62 83 | # 84 | # @api semipublic 85 | # 86 | # @example Encode a maximal KSUID as a string 87 | # KSUID::Base62.encode_bytes( 88 | # [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 89 | # 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] 90 | # ) 91 | # 92 | # @param bytes [String, Array] the bytes to encode 93 | # @return [String] the encoded bytes as a base 62 string 94 | def self.encode_bytes(bytes) 95 | encode(Utils.int_from_bytes(bytes)) 96 | end 97 | 98 | # Encodes a number as a string while disregarding the expected width 99 | # 100 | # @api private 101 | # 102 | # @param number [Integer] the number to encode 103 | # @return [String] the resulting encoded string 104 | def self.encode_without_padding(number) 105 | [].tap do |chars| 106 | loop do 107 | break unless number.positive? 108 | 109 | number, remainder = number.divmod(BASE) 110 | chars << CHARSET[remainder] 111 | end 112 | end 113 | end 114 | private_class_method :encode_without_padding 115 | 116 | # The character used as padding in strings that are less than 27 characters 117 | # 118 | # @api private 119 | # @return [String] 120 | def self.padding 121 | CHARSET[0] 122 | end 123 | private_class_method :padding 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /ksuid/lib/ksuid/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'securerandom' 4 | 5 | module KSUID 6 | # Encapsulates the configuration for the KSUID gem as a whole. 7 | # 8 | # You can override the generation of the random payload data by setting the 9 | # {#random_generator} value to a valid random payload generator. This should 10 | # be done via the module-level {KSUID.configure} method. 11 | # 12 | # The gem-level configuration lives at the module-level {KSUID.config}. 13 | # 14 | # @api semipublic 15 | class Configuration 16 | # Raised when the gem is misconfigured. 17 | ConfigurationError = Class.new(StandardError) 18 | 19 | # The default generator for generating random payloads 20 | # 21 | # @api private 22 | # 23 | # @return [Proc] 24 | def self.default_generator 25 | -> { SecureRandom.random_bytes(BYTES[:payload]) } 26 | end 27 | 28 | # Instantiates a new KSUID configuration 29 | # 30 | # @api private 31 | # 32 | # @return [KSUID::Configuration] the new configuration 33 | def initialize 34 | self.random_generator = self.class.default_generator 35 | end 36 | 37 | # The method for generating random payloads in the gem 38 | # 39 | # @api private 40 | # 41 | # @return [#call] a callable that returns 16 bytes 42 | attr_reader :random_generator 43 | 44 | # Override the method for generating random payloads in the gem 45 | # 46 | # @api semipublic 47 | # 48 | # @example Override the random generator with a null data generator 49 | # KSUID.configure do |config| 50 | # config.random_generator = -> { "\x00" * KSUID::BYTES[:payload] } 51 | # end 52 | # 53 | # @example Override the random generator with the faster, but less secure, Random 54 | # KSUID.configure do |config| 55 | # config.random_generator = -> { Random.new.bytes(KSUID::BYTES[:payload]) } 56 | # end 57 | # 58 | # @param generator [#call] a callable that returns 16 bytes 59 | # @return [#call] a callable that returns 16 bytes 60 | def random_generator=(generator) 61 | assert_generator_is_callable(generator) 62 | assert_payload_size(generator) 63 | 64 | @random_generator = generator 65 | end 66 | 67 | private 68 | 69 | # Raises an error if the assigned generator is not callable 70 | # 71 | # @api private 72 | # 73 | # @raise [ConfigurationError] if the generator is not callable 74 | # @return [nil] 75 | def assert_generator_is_callable(generator) 76 | return if generator.respond_to?(:call) 77 | 78 | raise ConfigurationError, "Random generator #{generator} is not callable" 79 | end 80 | 81 | # Raises an error if the assigned generator generates the wrong size 82 | # 83 | # @api private 84 | # 85 | # @raise [ConfigurationError] if the generator generates the wrong size payload 86 | # @return [nil] 87 | def assert_payload_size(generator) 88 | return if (length = generator.call.length) == (expected_length = BYTES[:payload]) 89 | 90 | raise( 91 | ConfigurationError, 92 | 'Random generator generates the wrong number of bytes ' \ 93 | "(#{length} generated, #{expected_length} expected)" 94 | ) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /ksuid/lib/ksuid/prefixed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module KSUID 4 | # Encapsulates the data type for a prefixed KSUID 5 | # 6 | # When you have different types of KSUIDs in your application, it can be 7 | # helpful to add an identifier to the front of them to give you an idea for 8 | # what kind of object the KSUID belongs to. 9 | # 10 | # For example, you might use KSUIDs to identify both Events and Customers. For 11 | # an Event, you could prefix the KSUID with the string `evt_`. Likewise, for 12 | # Customers, you could prefix them with the string `cus_`. 13 | # 14 | # {KSUID::Prefixed} gives you affordances for doing just this. 15 | # 16 | # ## Ordering 17 | # 18 | # {KSUID::Prefixed}s are partially orderable with {KSUID::Type} by their 19 | # timestamps. When ordering them with other {KSUID::Prefixed} instances, they 20 | # order first by prefix, then by timestamp. This means that in a mixed 21 | # collection, all Customer KSUIDs (prefix: `cus_`) would be grouped before all 22 | # Event KSUIDs (prefix `evt_`). 23 | # 24 | # ## Interface 25 | # 26 | # You typically will not instantiate this class directly, but instead use the 27 | # {KSUID.prefixed} builder method to save some typing. 28 | # 29 | # The most commonly used helper methods for the {KSUID} module also exist on 30 | # {KSUID::Prefixed} for converting between different forms of output. 31 | # 32 | # ## Differences from {KSUID::Type} 33 | # 34 | # One other thing to note is that {KSUID::Prefixed} is not intended to handle 35 | # binary data because the prefix does not make sense in either the byte string 36 | # or packed array formats. 37 | # 38 | # @since 0.5.0 39 | class Prefixed < Type 40 | include Comparable 41 | 42 | # Converts a KSUID-compatible value into a {KSUID::Prefixed} 43 | # 44 | # @api public 45 | # 46 | # @example Converts a base 62 KSUID string into a {KSUID::Prefixed} 47 | # KSUID::Prefixed.call('15Ew2nYeRDscBipuJicYjl970D1', prefix: 'evt_') 48 | # 49 | # @param ksuid [String, KSUID::Prefixed, KSUID::Type] the prefixed KSUID-compatible value 50 | # @return [KSUID::Prefixed] the converted, prefixed KSUID 51 | # @raise [ArgumentError] if the value is not prefixed KSUID-compatible 52 | def self.call(ksuid, prefix:) 53 | return unless ksuid && prefix 54 | 55 | case ksuid 56 | when KSUID::Prefixed then from_base62(ksuid.to_ksuid.to_s, prefix: prefix) 57 | when KSUID::Type then from_base62(ksuid.to_s, prefix: prefix) 58 | when String then cast_string(ksuid, prefix: prefix) 59 | else 60 | raise ArgumentError, "Cannot convert #{ksuid.inspect} to KSUID::Prefixed" 61 | end 62 | end 63 | 64 | # Converts a base 62-encoded string into a {KSUID::Prefixed} 65 | # 66 | # @api public 67 | # 68 | # @example Parse a KSUID string into a prefixed object 69 | # KSUID::Prefixed.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW', prefix: 'evt_') 70 | # 71 | # @param string [String] the base 62-encoded KSUID to convert into an object 72 | # @param prefix [String] the prefix to add to the KSUID 73 | # @return [KSUID::Prefixed] the prefixed KSUID generated from the string 74 | def self.from_base62(string, prefix:) 75 | string = string.sub(/\A#{prefix}/, '') 76 | int = Base62.decode(string) 77 | bytes = Utils.int_to_bytes(int, 160) 78 | 79 | from_bytes(bytes, prefix: prefix) 80 | end 81 | 82 | # Casts a string into a {KSUID::Prefixed} 83 | # 84 | # @api private 85 | # 86 | # @param ksuid [String] the string to convert into a {KSUID::Prefixed} 87 | # @param prefix [String] the prefix to prepend to the KSUID 88 | # @return [KSUID::Prefixed] the converted, prefixed KSUID 89 | def self.cast_string(ksuid, prefix:) 90 | ksuid = ksuid[-KSUID::BYTES[:base62]..-1] if ksuid.length >= KSUID::BYTES[:base62] 91 | 92 | unless Base62.compatible?(ksuid) 93 | raise ArgumentError, 'Prefixed KSUIDs cannot be binary strings' 94 | end 95 | 96 | from_base62(ksuid, prefix: prefix) 97 | end 98 | private_class_method :cast_string 99 | 100 | # Converts a byte string or byte array into a KSUID 101 | # 102 | # @api private 103 | # 104 | # @param bytes [String] the byte string to convert into an object 105 | # @return [KSUID::Prefixed] the prefixed KSUID generated from the bytes 106 | def self.from_bytes(bytes, prefix:) 107 | bytes = bytes.bytes 108 | timestamp = Utils.int_from_bytes(bytes.first(KSUID::BYTES[:timestamp])) 109 | payload = Utils.byte_string_from_array(bytes.last(KSUID::BYTES[:payload])) 110 | 111 | new(prefix, payload: payload, time: Time.at(timestamp + EPOCH_TIME)) 112 | end 113 | private_class_method :from_bytes 114 | 115 | # Instantiates a new {KSUID::Prefixed} 116 | # 117 | # @api semipublic 118 | # 119 | # @example Generate a new {KSUID::Prefixed} for the current second 120 | # KSUID::Prefixed.new('evt_') 121 | # 122 | # @example Generate a new {KSUID::Prefixed} for a given timestamp 123 | # KSUID::Prefixed.new('cus_', time: Time.parse('2017-11-05 15:00:04 UTC')) 124 | # 125 | # @param prefix [String] the prefix to add to the KSUID 126 | # @param payload [String, Array, nil] the payload for the KSUID 127 | # @param time [Time] the timestamp to use for the KSUID 128 | # @return [KSUID::Prefix] the generated, prefixed KSUID 129 | def initialize(prefix, payload: nil, time: Time.now) 130 | raise ArgumentError, 'requires a prefix' unless prefix 131 | 132 | super(payload: payload, time: time) 133 | 134 | @prefix = prefix 135 | end 136 | 137 | # The prefix in front of the KSUID 138 | # 139 | # @api semipublic 140 | # 141 | # @example Getting the prefix to create a similar {KSUID::Prefixed} 142 | # ksuid1 = KSUID.prefixed('cus_') 143 | # ksuid2 = KSUID.prefixed(ksuid1.prefix) 144 | # 145 | # @return [String] the prefix of the {KSUID::Prefixed} 146 | attr_reader :prefix 147 | 148 | # Implements the Comparable interface for sorting {KSUID::Prefixed}s 149 | # 150 | # @api private 151 | # 152 | # @param other [KSUID::Type] the other object to compare against 153 | # @return [Integer, nil] nil for uncomparable, -1 for less than other, 154 | # 0 for equal to, 1 for greater than other 155 | def <=>(other) 156 | return unless other.is_a?(Type) 157 | return super if other.instance_of?(Type) 158 | 159 | if (result = prefix <=> other.prefix).nonzero? 160 | result 161 | else 162 | super 163 | end 164 | end 165 | 166 | # Checks whether this {KSUID::Prefixed} is equal to another 167 | # 168 | # @api semipublic 169 | # 170 | # @example Checks whether two KSUIDs are equal 171 | # KSUID.prefixed('evt_') == KSUID.prefixed('evt_') 172 | # 173 | # @param other [KSUID::Prefixed] the other {KSUID::Prefixed} to check against 174 | # @return [Boolean] 175 | def ==(other) 176 | other.is_a?(Prefixed) && 177 | prefix == other.prefix && 178 | super 179 | end 180 | 181 | # Generates the key to use when using a {KSUID::Prefixed} as a hash key 182 | # 183 | # @api semipublic 184 | # 185 | # @example Using a KSUID as a Hash key 186 | # ksuid1 = KSUID.prefixed('evt_') 187 | # ksuid2 = ksuid1.dup 188 | # values_by_ksuid = {} 189 | # 190 | # values_by_ksuid[ksuid1] = "example" 191 | # values_by_ksuid[ksuid2] #=> "example" 192 | # 193 | # @return [Integer] 194 | def hash 195 | [prefix, @uid].hash 196 | end 197 | 198 | # The {KSUID::Prefixed} as a prefixed, hex-encoded string 199 | # 200 | # This is generally useful for comparing against the Go tool. 201 | # 202 | # @api public 203 | # 204 | # @example 205 | # ksuid = KSUID::Prefixed.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW', prefix: 'evt_') 206 | # 207 | # ksuid.raw #=> "evt_0683F789049CC215C099D42B784DBE99341BD79C" 208 | # 209 | # @return [String] a prefixed, hex-encoded string 210 | def raw 211 | super.prepend(prefix) 212 | end 213 | 214 | # Converts the {KSUID::Prefixed} into a {KSUID::Type} by dropping the prefix 215 | # 216 | # @api public 217 | # 218 | # @example Convert an Event KSUID into a plain KSUID 219 | # ksuid = KSUID.prefixed('evt_') 220 | # 221 | # ksuid.to_ksuid 222 | # 223 | # @return [KSUID::Type] the non-prefixed KSUID 224 | def to_ksuid 225 | KSUID.from_base62(to_s.sub(/\A#{prefix}/, '')) 226 | end 227 | 228 | # The {KSUID::Prefixed} as a base 62-encoded string 229 | # 230 | # @api public 231 | # 232 | # @example 233 | # ksuid = KSUID::Prefixed.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW', prefix: 'evt_') 234 | # 235 | # ksuid.to_s #=> "evt_0vdbMgWkU6slGpLVCqEFwkkZvuW" 236 | # 237 | # @return [String] the prefixed, base 62-encoded string for the {KSUID::Prefixed} 238 | def to_s 239 | super.prepend(prefix) 240 | end 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /ksuid/lib/ksuid/type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module KSUID 4 | # Encapsulates the data type for a KSUID 5 | # 6 | # This is the main class that you will interact with in this gem. You will 7 | # not typically generate these directly, but this is the resulting data type 8 | # for all of the main generation methods on the {KSUID} module. 9 | # 10 | # A KSUID type has two pieces of information contained within its 11 | # byte-encoded data: 12 | # 13 | # 1. The timestamp associated with the KSUID (stored as the first 4 bytes) 14 | # 2. The payload, or random data, for the KSUID (stored as the last 16 bytes) 15 | # 16 | # The type gives you access to several handles into these data. 17 | class Type 18 | include Comparable 19 | 20 | # Instantiates a new KSUID type 21 | # 22 | # @api semipublic 23 | # 24 | # @example Generate a new KSUID for the current second 25 | # KSUID::Type.new 26 | # 27 | # @example Generate a new KSUID for a given timestamp 28 | # KSUID::Type.new(time: Time.parse('2017-11-05 15:00:04 UTC')) 29 | # 30 | # @param payload [String, Array, nil] the payload for the KSUID 31 | # @param time [Time] the timestamp to use for the KSUID 32 | # @return [KSUID::Type] the generated KSUID 33 | def initialize(payload: nil, time: Time.now) 34 | payload ||= KSUID.config.random_generator.call 35 | byte_encoding = Utils.int_to_bytes(time.to_i - EPOCH_TIME) 36 | 37 | @uid = byte_encoding.bytes + payload.bytes 38 | end 39 | 40 | # Implements the Comparable interface for sorting KSUIDs 41 | # 42 | # @api private 43 | # 44 | # @param other [KSUID::Type] the other object to compare against 45 | # @return [Integer] -1 for less than other, 0 for equal to, 1 for greater than other 46 | def <=>(other) 47 | to_time <=> other.to_time 48 | end 49 | 50 | # Checks whether this KSUID is equal to another 51 | # 52 | # @api semipublic 53 | # 54 | # @example Checks whether two KSUIDs are equal 55 | # KSUID.new == KSUID.new 56 | # 57 | # @param other [KSUID::Type] the other KSUID to check against 58 | # @return [Boolean] 59 | def ==(other) 60 | other.to_s == to_s 61 | end 62 | 63 | # Checks whether this KSUID hashes to the same hash key as another 64 | # 65 | # @api semipublic 66 | # 67 | # @example Checks whether two KSUIDs hash to the same key 68 | # KSUID.new.eql? KSUID.new 69 | # 70 | # @param other [KSUID::Type] the other KSUID to check against 71 | # @return [Boolean] 72 | def eql?(other) 73 | hash == other.hash 74 | end 75 | 76 | # Generates the key to use when using a KSUID as a hash key 77 | # 78 | # @api semipublic 79 | # 80 | # @example Using a KSUID as a Hash key 81 | # ksuid1 = KSUID.new 82 | # ksuid2 = KSUID.from_base62(ksuid1.to_s) 83 | # values_by_ksuid = {} 84 | # 85 | # values_by_ksuid[ksuid1] = "example" 86 | # values_by_ksuid[ksuid2] #=> "example" 87 | # 88 | # @return [Integer] 89 | def hash 90 | @uid.hash 91 | end 92 | 93 | # Prints the KSUID for debugging within a console 94 | # 95 | # @api public 96 | # 97 | # @example Show the maximum KSUID 98 | # KSUID.max.inspect #=> "" 99 | # 100 | # @return [String] 101 | def inspect 102 | "" 103 | end 104 | 105 | # The payload for the KSUID, as a hex-encoded string 106 | # 107 | # This is generally useful for comparing against the Go tool 108 | # 109 | # @api public 110 | # 111 | # @example 112 | # ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW') 113 | # 114 | # ksuid.payload #=> "049CC215C099D42B784DBE99341BD79C" 115 | # 116 | # @return [String] a hex-encoded string 117 | def payload 118 | Utils.bytes_to_hex_string(uid.last(BYTES[:payload])) 119 | end 120 | 121 | # The KSUID as a hex-encoded string 122 | # 123 | # This is generally useful for comparing against the Go tool. 124 | # 125 | # @api public 126 | # 127 | # @example 128 | # ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW') 129 | # 130 | # ksuid.raw #=> "0683F789049CC215C099D42B784DBE99341BD79C" 131 | # 132 | # @return [String] a hex-encoded string 133 | def raw 134 | Utils.bytes_to_hex_string(uid) 135 | end 136 | 137 | # The KSUID as a byte string 138 | # 139 | # @api public 140 | # 141 | # @example 142 | # ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW') 143 | # 144 | # ksuid.to_bytes 145 | # 146 | # @return [String] a byte string 147 | def to_bytes 148 | Utils.byte_string_from_array(uid) 149 | end 150 | 151 | # The KSUID as a Unix timestamp 152 | # 153 | # @api public 154 | # 155 | # @example 156 | # ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW') 157 | # 158 | # ksuid.to_i #=> 109311881 159 | # 160 | # @return [Integer] the Unix timestamp for the event (without the epoch shift) 161 | def to_i 162 | Utils.int_from_bytes(uid.first(BYTES[:timestamp])) 163 | end 164 | 165 | # The KSUID as a base 62-encoded string 166 | # 167 | # @api public 168 | # 169 | # @example 170 | # ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW') 171 | # 172 | # ksuid.to_s #=> "0vdbMgWkU6slGpLVCqEFwkkZvuW" 173 | # 174 | # @return [String] the base 62-encoded string for the KSUID 175 | def to_s 176 | Base62.encode_bytes(uid) 177 | end 178 | 179 | # The time the KSUID was generated 180 | # 181 | # @api public 182 | # 183 | # @example 184 | # ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW') 185 | # 186 | # ksuid.to_time.utc.to_s #=> "2017-10-29 21:18:01 UTC" 187 | # 188 | # @return [String] the base 62-encoded string for the KSUID 189 | def to_time 190 | Time.at(to_i + EPOCH_TIME) 191 | end 192 | 193 | private 194 | 195 | # The KSUID as a byte array 196 | # 197 | # @api private 198 | # 199 | # @return [Array] 200 | attr_reader :uid 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /ksuid/lib/ksuid/utils.rb: -------------------------------------------------------------------------------- 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 | # A regular expression for splitting bytes out of a "binary" string 9 | # 10 | # @api private 11 | # @return [Regexp] the splitter 12 | BYTES = /.{8}/.freeze 13 | 14 | # A regular expression for splitting a String into pairs of characters 15 | # 16 | # @api private 17 | # @return [Regexp] the splitter 18 | PAIRS = /.{2}/.freeze 19 | 20 | # Converts a byte array into a byte string 21 | # 22 | # @param bytes [String] a byte string 23 | # @return [Array] an array of bytes from the byte string 24 | def self.byte_string_from_array(bytes) 25 | bytes.pack('C*') 26 | end 27 | 28 | # Converts a hex string into a byte string 29 | # 30 | # @param hex [String] a hex-encoded KSUID 31 | # @param bits [Integer] the expected number of bits for the result 32 | # @return [String] the byte string 33 | def self.byte_string_from_hex(hex, bits = 32) 34 | byte_array = 35 | hex 36 | .rjust(bits, '0') 37 | .scan(PAIRS) 38 | .map { |bytes| bytes.to_i(16) } 39 | 40 | byte_string_from_array(byte_array) 41 | end 42 | 43 | # Converts a byte string or byte array into a hex-encoded string 44 | # 45 | # @param bytes [String, Array] the byte string or array 46 | # @return [String] the byte string as a hex-encoded string 47 | def self.bytes_to_hex_string(bytes) 48 | bytes = bytes.bytes if bytes.is_a?(String) 49 | 50 | byte_string_from_array(bytes) 51 | .unpack1('H*') 52 | .upcase 53 | end 54 | 55 | # Converts a byte string or byte array into an integer 56 | # 57 | # @param bytes [String, Array] the byte string or array 58 | # @return [Integer] the resulting integer 59 | def self.int_from_bytes(bytes) 60 | bytes = bytes.bytes if bytes.is_a?(String) 61 | 62 | bytes 63 | .map { |byte| byte.to_s(2).rjust(8, '0') } 64 | .join 65 | .to_i(2) 66 | end 67 | 68 | # Converts an integer into a network-ordered (big endian) byte string 69 | # 70 | # @param int [Integer] the integer to convert 71 | # @param bits [Integer] the expected number of bits for the result 72 | # @return [String] the byte string 73 | def self.int_to_bytes(int, bits = 32) 74 | int 75 | .to_s(2) 76 | .rjust(bits, '0') 77 | .scan(BYTES) 78 | .map { |digits| digits.to_i(2) } 79 | .pack("C#{bits / 8}") 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /ksuid/lib/ksuid/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module KSUID 4 | # The version of the KSUID gem 5 | # 6 | # @return [String] 7 | VERSION = '1.0.0' 8 | end 9 | -------------------------------------------------------------------------------- /ksuid/spec/compatibility_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'time' 4 | 5 | RSpec.describe 'compatibility tests', type: :compatibility do 6 | it 'handles the maximum properly', :aggregate_failures do 7 | ksuid = KSUID.from_base62('aWgEPTl1tmebfsQzFP4bxwgy80V') 8 | 9 | expect(ksuid.to_s).to eq('aWgEPTl1tmebfsQzFP4bxwgy80V') 10 | expect(ksuid.to_time).to eq(Time.parse('2150-06-19 17:21:35 -0600 CST')) 11 | expect(ksuid.to_i).to eq(4_294_967_295) 12 | expect(ksuid.payload).to eq('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') 13 | expect(ksuid.raw).to eq('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') 14 | end 15 | 16 | it 'handles the minimum properly', :aggregate_failures do 17 | ksuid = KSUID.from_base62('000000000000000000000000000') 18 | 19 | expect(ksuid.to_s).to eq('000000000000000000000000000') 20 | expect(ksuid.to_time).to eq(Time.parse('2014-05-13 11:53:20 -0500 CDT')) 21 | expect(ksuid.to_i).to eq(0) 22 | expect(ksuid.payload).to eq('00000000000000000000000000000000') 23 | expect(ksuid.raw).to eq('0000000000000000000000000000000000000000') 24 | end 25 | 26 | it 'handles an example value', :aggregate_failures do 27 | ksuid = KSUID.from_base62('0vdbMgWkU6slGpLVCqEFwkkZvuW') 28 | 29 | expect(ksuid.to_s).to eq('0vdbMgWkU6slGpLVCqEFwkkZvuW') 30 | expect(ksuid.raw).to eq('0683F789049CC215C099D42B784DBE99341BD79C') 31 | expect(ksuid.to_time).to eq(Time.parse('2017-10-29 16:18:01 -0500 CDT')) 32 | expect(ksuid.to_i).to eq(109_311_881) 33 | expect(ksuid.payload).to eq('049CC215C099D42B784DBE99341BD79C') 34 | end 35 | 36 | it 'handles another example value', :aggregate_failures do 37 | ksuid = KSUID.from_base62('0vdbMkSk7XwvMeKS6aZMM2AVZ4G') 38 | 39 | expect(ksuid.to_s).to eq('0vdbMkSk7XwvMeKS6aZMM2AVZ4G') 40 | expect(ksuid.to_time).to eq(Time.parse('2017-10-29 16:18:01 -0500 CDT')) 41 | expect(ksuid.to_i).to eq(109_311_881) 42 | expect(ksuid.payload).to eq('85EAB6C3F1809D7D4A00760CCBF7707C') 43 | expect(ksuid.raw).to eq('0683F78985EAB6C3F1809D7D4A00760CCBF7707C') 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /ksuid/spec/doctest_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ksuid' 4 | -------------------------------------------------------------------------------- /ksuid/spec/ksuid/base62_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe KSUID::Base62 do 4 | describe '#compatible?' do 5 | it 'correctly detects invalid values in a string', :aggregate_failures do 6 | expect(described_class.compatible?('15Ew2nYeRDscBipuJicYjl970D1')).to be true 7 | expect(described_class.compatible?(("\xFF" * 20).b)).not_to be true 8 | end 9 | end 10 | 11 | describe '#decode' do 12 | it 'decodes base 62 numbers that may or may not be zero-padded' do 13 | %w[awesomesauce 00000000awesomesauce].each do |encoded| 14 | decoded = described_class.decode(encoded) 15 | 16 | expect(decoded).to eq(1_922_549_000_510_644_890_748) 17 | end 18 | end 19 | 20 | it 'decodes zero' do 21 | encoded = '0' 22 | 23 | decoded = described_class.decode(encoded) 24 | 25 | expect(decoded).to eq(0) 26 | end 27 | 28 | it 'decodes numbers that are longer than 20 digits' do 29 | encoded = '01234567890123456789' 30 | 31 | decoded = described_class.decode(encoded) 32 | 33 | expect(decoded).to eq(189_310_246_048_642_169_039_429_477_271_925) 34 | end 35 | 36 | it 'does bad things for words that are not base 62' do 37 | expect { described_class.decode('this should break!') }.to raise_error(ArgumentError) 38 | end 39 | end 40 | 41 | describe '#encode' do 42 | it 'encodes numbers into 27-digit base 62' do 43 | number = 1_922_549_000_510_644_890_748 44 | 45 | encoded = described_class.encode(number) 46 | 47 | expect(encoded).to eq('000000000000000awesomesauce') 48 | end 49 | 50 | it 'encodes negative numbers as zero' do 51 | number = -1 52 | 53 | encoded = described_class.encode(number) 54 | 55 | expect(encoded).to eq('000000000000000000000000000') 56 | end 57 | end 58 | 59 | describe '#encode_bytes' do 60 | it 'encodes byte strings' do 61 | bytes = "\xFF" * 4 62 | 63 | encoded = described_class.encode_bytes(bytes) 64 | 65 | expect(encoded).to eq('0000000000000000000004gfFC3') 66 | end 67 | 68 | it 'encodes byte arrays' do 69 | bytes = [255] * 4 70 | 71 | encoded = described_class.encode_bytes(bytes) 72 | 73 | expect(encoded).to eq('0000000000000000000004gfFC3') 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /ksuid/spec/ksuid/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe KSUID::Configuration do 4 | describe '#random_generator' do 5 | it 'defaults to using secure random' do 6 | config = described_class.new 7 | 8 | random = config.random_generator.call 9 | 10 | expect(random.size).to eq(16) 11 | end 12 | 13 | it 'can be overridden with a proper random generator' do 14 | config = described_class.new 15 | config.random_generator = -> { Random.new.bytes(16) } 16 | 17 | random = config.random_generator.call 18 | 19 | expect(random.size).to eq(16) 20 | end 21 | 22 | it 'cannot be overridden by a non-callable' do 23 | config = described_class.new 24 | 25 | expect { config.random_generator = 'Hello' }.to raise_error( 26 | KSUID::Configuration::ConfigurationError 27 | ) 28 | end 29 | 30 | it 'cannot be overriden by a generator of the wrong length' do 31 | config = described_class.new 32 | short_generator = -> { Random.new.bytes(5) } 33 | 34 | expect { config.random_generator = short_generator }.to raise_error( 35 | KSUID::Configuration::ConfigurationError 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /ksuid/spec/ksuid/prefixed_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe KSUID::Prefixed do 4 | describe 'value object semantics' do 5 | it 'uses value comparison instead of identity comparison' do 6 | ksuid1 = KSUID.prefixed('evt_') 7 | ksuid2 = ksuid1.dup 8 | hash = {} 9 | 10 | hash[ksuid1] = 'Hello, world' 11 | 12 | aggregate_failures do 13 | expect(ksuid1).to eq ksuid2 14 | expect(ksuid1).to eql ksuid2 15 | expect(ksuid1).not_to equal ksuid2 16 | expect(hash[ksuid2]).to eq 'Hello, world' 17 | end 18 | end 19 | end 20 | 21 | describe '.call' do 22 | it 'returns same-prefixed KSUIDs in tact' do 23 | ksuid = KSUID.new 24 | 25 | result = KSUID.call(ksuid) 26 | 27 | expect(result).to eq(ksuid) 28 | end 29 | 30 | it 'prefixes KSUIDs' do 31 | ksuid = KSUID.new 32 | 33 | result = described_class.call(ksuid, prefix: 'evt_') 34 | 35 | expect(result.to_s).to eq("evt_#{ksuid}") 36 | end 37 | 38 | it 'raises for byte strings' do 39 | ksuid = KSUID.prefixed('evt_') 40 | 41 | expect { described_class.call(ksuid.to_bytes, prefix: 'evt_') } 42 | .to raise_error(ArgumentError) 43 | end 44 | 45 | it 'raises for byte arrays' do 46 | ksuid = KSUID.prefixed('evt_') 47 | 48 | expect { described_class.call(ksuid.__send__(:uid), prefix: 'evt_') } 49 | .to raise_error(ArgumentError) 50 | end 51 | 52 | it 'converts base 62 strings to KSUIDs' do 53 | ksuid = KSUID.new 54 | 55 | result = described_class.call(ksuid.to_s, prefix: 'cus_') 56 | 57 | expect(result.to_s).to eq("cus_#{ksuid}") 58 | end 59 | 60 | it 'returns nil if passed nil' do 61 | result = described_class.call(nil, prefix: 'evt_') 62 | 63 | expect(result).to be_nil 64 | end 65 | 66 | it 'raise an ArgumentError upon an unknown value' do 67 | expect { described_class.call(1, prefix: 'evt_') } 68 | .to raise_error(ArgumentError) 69 | end 70 | end 71 | 72 | describe '#<=>' do 73 | it 'does not sort with non-KSUIDs' do 74 | ksuid = KSUID.prefixed('evt_') 75 | 76 | expect(ksuid <=> ksuid.to_s).to be_nil 77 | end 78 | 79 | it 'sorts with un-prefixed KSUIDs by time' do 80 | ksuid1 = KSUID.prefixed('evt_', time: Time.parse('2022-08-16 11:00:00 UTC')) 81 | ksuid2 = KSUID.new(time: Time.parse('2022-08-16 10:00:00 UTC')) 82 | ksuid3 = KSUID.new(time: Time.parse('2022-08-16 12:00:00 UTC')) 83 | 84 | sorted = [ksuid1, ksuid2, ksuid3].sort 85 | 86 | expect(sorted).to eq([ksuid2, ksuid1, ksuid3]) 87 | end 88 | 89 | it 'sorts with prefixed KSUIDs by prefix, then time' do 90 | ksuid1 = KSUID.prefixed('evt_', time: Time.parse('2022-08-16 11:00:00 UTC')) 91 | ksuid2 = KSUID.prefixed('evt_', time: Time.parse('2022-08-16 10:00:00 UTC')) 92 | ksuid3 = KSUID.prefixed('cus_', time: Time.parse('2022-08-16 12:00:00 UTC')) 93 | 94 | sorted = [ksuid1, ksuid2, ksuid3].sort 95 | 96 | expect(sorted).to eq([ksuid3, ksuid2, ksuid1]) 97 | end 98 | end 99 | 100 | describe '#==' do 101 | it 'requires a prefixed KSUID' do 102 | ksuid1 = KSUID.prefixed('evt_', time: Time.parse('2022-08-16 11:00:00 UTC')) 103 | ksuid2 = KSUID.call(ksuid1) 104 | 105 | expect(ksuid1).not_to eq(ksuid2) 106 | end 107 | 108 | it 'checks the prefix as well as the uid', :aggregate_failures do 109 | ksuid1 = KSUID.prefixed('evt_', time: Time.parse('2022-08-16 11:00:00 UTC')) 110 | ksuid2 = described_class.call(ksuid1, prefix: 'evt_') 111 | ksuid3 = described_class.call(ksuid1, prefix: 'cus_') 112 | 113 | expect(ksuid1).to eq(ksuid2) 114 | expect(ksuid1).not_to eq(ksuid3) 115 | end 116 | end 117 | 118 | describe '#raw' do 119 | it 'prefixes the original KSUID payload' do 120 | ksuid = described_class.from_base62('evt_0vdbMgWkU6slGpLVCqEFwkkZvuW', prefix: 'evt_') 121 | 122 | expect(ksuid.raw).to eq('evt_0683F789049CC215C099D42B784DBE99341BD79C') 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /ksuid/spec/ksuid/type_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe KSUID::Type do 4 | describe 'value object semantics' do 5 | it 'uses value comparison instead of identity comparison' do 6 | ksuid1 = KSUID.new(time: Time.now) 7 | ksuid2 = KSUID.from_base62(ksuid1.to_s) 8 | hash = {} 9 | 10 | hash[ksuid1] = 'Hello, world' 11 | 12 | aggregate_failures do 13 | expect(ksuid1).to eq ksuid2 14 | expect(ksuid1).to eql ksuid2 15 | expect(ksuid1).not_to equal ksuid2 16 | expect(hash[ksuid2]).to eq 'Hello, world' 17 | end 18 | end 19 | end 20 | 21 | describe '.from_base62' do 22 | it 'converts a base62 KSUID properly' do 23 | ksuid = KSUID.from_base62(KSUID::MAX_STRING_ENCODED) 24 | 25 | expect(ksuid).to eq(KSUID.max) 26 | end 27 | end 28 | 29 | describe '#<=>' do 30 | it 'sorts the KSUIDs by timestamp' do 31 | ksuid1 = KSUID.new(time: Time.now) 32 | ksuid2 = KSUID.new(time: Time.now + 1) 33 | 34 | array = [ksuid2, ksuid1].sort 35 | 36 | expect(array).to eq([ksuid1, ksuid2]) 37 | end 38 | end 39 | 40 | describe '#==' do 41 | it 'matches against other KSUID::Types as well as String' do 42 | ksuid1 = KSUID.new(time: Time.now) 43 | ksuid2 = KSUID.from_base62(ksuid1.to_s) 44 | 45 | aggregate_failures do 46 | expect(ksuid1).to eq ksuid2 47 | expect(ksuid1).to eq ksuid2.to_s 48 | end 49 | end 50 | end 51 | 52 | describe '#inspect' do 53 | it 'shows the string representation for easy understanding' do 54 | ksuid = KSUID.max 55 | 56 | expect(ksuid.inspect).to match('aWgEPTl1tmebfsQzFP4bxwgy80V') 57 | end 58 | end 59 | 60 | describe '#payload' do 61 | it 'returns the payload as a byte string' do 62 | expected = 'F' * 32 63 | 64 | array = KSUID.max.payload 65 | 66 | expect(array).to eq(expected) 67 | end 68 | end 69 | 70 | describe '#to_bytes' do 71 | it 'returns the ksuid as a byte string' do 72 | expected = ("\xFF" * 20).bytes 73 | 74 | array = KSUID.max.to_bytes.bytes 75 | 76 | expect(array).to eq(expected) 77 | end 78 | end 79 | 80 | describe '#to_time' do 81 | it 'returns the times used to create the ksuid' do 82 | time = Time.at(Time.now.to_i) 83 | 84 | ksuid = KSUID.new(time: time) 85 | 86 | expect(ksuid.to_time).to eq(time) 87 | end 88 | end 89 | 90 | describe '#to_s' do 91 | it 'correctly represents the maximum value' do 92 | expect(KSUID.max.to_s).to eq(KSUID::MAX_STRING_ENCODED) 93 | end 94 | 95 | it 'correctly represents zero' do 96 | expected = '0' * 27 97 | 98 | string = KSUID.from_bytes([0] * 20).to_s 99 | 100 | expect(string).to eq(expected) 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /ksuid/spec/ksuid/utils_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe KSUID::Utils do 4 | it 'can convert between integers and bytes losslessly' do 5 | number = 123_456_789 6 | bytes = described_class.int_to_bytes(number) 7 | converted_number = described_class.int_from_bytes(bytes) 8 | 9 | expect(converted_number).to eq(number) 10 | end 11 | 12 | describe '#byte_string_from_hex' do 13 | it 'converts a hex string to an integer', :aggregate_failures do 14 | hex = '0DE978D96CA064CB84C244311C261F49DB083AA8' 15 | 16 | result = described_class.byte_string_from_hex(hex) 17 | 18 | expect(described_class.bytes_to_hex_string(result)).to eq hex 19 | expect(KSUID.call(result)).to eq KSUID.call('1z4PxXDcFiwInVMCTC3MvcbGptw') 20 | end 21 | end 22 | 23 | describe '#int_from_bytes' do 24 | it 'converts a byte string to an integer' do 25 | number_from_binary = ('1' * 32).to_i(2) 26 | byte_string = "\xFF" * 4 27 | 28 | converted_number = described_class.int_from_bytes(byte_string) 29 | 30 | expect(converted_number).to eq(number_from_binary) 31 | end 32 | 33 | it 'converts a byte array to an integer' do 34 | number_from_binary = ('1' * 32).to_i(2) 35 | byte_array = [255] * 4 36 | 37 | converted_number = described_class.int_from_bytes(byte_array) 38 | 39 | expect(converted_number).to eq(number_from_binary) 40 | end 41 | 42 | it 'handles the maximum ksuid' do 43 | expected = 1_461_501_637_330_902_918_203_684_832_716_283_019_655_932_542_975 44 | 45 | converted = described_class.int_from_bytes([255] * 20) 46 | 47 | expect(converted).to eq(expected) 48 | end 49 | end 50 | 51 | describe '#int_to_bytes' do 52 | it 'converts an integer to a byte string' do 53 | number_from_binary = ('1' * 32).to_i(2) 54 | expected = ("\xFF" * 4).bytes 55 | 56 | converted_bytes = described_class.int_to_bytes(number_from_binary).bytes 57 | 58 | expect(converted_bytes).to eq(expected) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /ksuid/spec/ksuid_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe KSUID do 4 | it 'has a version number' do 5 | expect(KSUID::VERSION).not_to be_nil 6 | end 7 | 8 | it 'is configurable' do 9 | generator = -> { "\x00" * KSUID::BYTES[:payload] } 10 | 11 | described_class.configure { |config| config.random_generator = generator } 12 | 13 | expect(described_class.config.random_generator).to eq(generator) 14 | ensure 15 | described_class.configure do |config| 16 | config.random_generator = KSUID::Configuration.default_generator 17 | end 18 | end 19 | 20 | describe '.call' do 21 | it 'returns KSUIDs in tact' do 22 | ksuid = described_class.new 23 | 24 | result = described_class.call(ksuid) 25 | 26 | expect(result).to eq(ksuid) 27 | end 28 | 29 | it 'converts byte strings to KSUIDs' do 30 | ksuid = described_class.new 31 | 32 | result = described_class.call(ksuid.to_bytes) 33 | 34 | expect(result).to eq(ksuid) 35 | end 36 | 37 | it 'converts byte arrays to KSUIDs' do 38 | ksuid = described_class.new 39 | 40 | result = described_class.call(ksuid.__send__(:uid)) 41 | 42 | expect(result).to eq(ksuid) 43 | end 44 | 45 | it 'converts base 62 strings to KSUIDs' do 46 | ksuid = described_class.new 47 | 48 | result = described_class.call(ksuid.to_s) 49 | 50 | expect(result).to eq(ksuid) 51 | end 52 | 53 | it 'returns nil if passed nil' do 54 | result = described_class.call(nil) 55 | 56 | expect(result).to be_nil 57 | end 58 | 59 | it 'raise an ArgumentError upon an unknown value' do 60 | expect { described_class.call(1) }.to raise_error(ArgumentError) 61 | end 62 | end 63 | 64 | describe '.string' do 65 | it 'uses the current time and a random payload by default' do 66 | string = described_class.string 67 | 68 | expect(string.length).to eq 27 69 | end 70 | 71 | it 'accepts a payload and a time' do 72 | string = described_class.string( 73 | payload: ("\xFF" * KSUID::BYTES[:payload]), 74 | time: Time.new(2150, 6, 19, 23, 21, 35, '+00:00') 75 | ) 76 | 77 | expect(string).to eq KSUID::MAX_STRING_ENCODED 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /ksuid/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV['COVERAGE'] || ENV['CI'] 4 | require 'simplecov' 5 | 6 | SimpleCov.start do 7 | add_filter '/spec/' 8 | end 9 | end 10 | 11 | begin 12 | require 'pry' 13 | rescue LoadError # rubocop:disable Lint/SuppressedException 14 | end 15 | 16 | require 'ksuid' 17 | 18 | RSpec.configure do |config| 19 | config.expect_with :rspec do |expectations| 20 | expectations.syntax = :expect 21 | end 22 | 23 | config.mock_with :rspec do |mocks| 24 | mocks.verify_partial_doubles = true 25 | end 26 | 27 | config.disable_monkey_patching! 28 | config.example_status_persistence_file_path = 'spec/examples.txt' 29 | config.filter_run_when_matching :focus 30 | config.shared_context_metadata_behavior = :apply_to_host_groups 31 | config.warnings = true 32 | 33 | config.default_formatter = 'doc' if config.files_to_run.one? 34 | config.profile_examples = 10 if ENV['PROFILE'] 35 | 36 | config.order = :random 37 | Kernel.srand config.seed 38 | end 39 | --------------------------------------------------------------------------------