rails-clusterid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6dbfb92bb8859432533b75b073f0503ed9d295fa57ed63f6bd61d5ffa16b3cd9
4
+ data.tar.gz: e12b51e557e301536cecfae29299212b57de2ef9dd87b935907426e4f3e7446b
5
+ SHA512:
6
+ metadata.gz: ae5314e6cec2279525198c1296d4c989ef3a8897a15e767da8653706b6039173dd2ce1544883431c2dccf3e0b317ff2e02580e418476cfdb4e36f16e48cdaba3
7
+ data.tar.gz: cca1e0a4c03ef8a969027ea4e41596836b2be77812e58506ba23c55664e86989fb49d40d7dbe79a11752212c553be6a635843c16f4fde5374bd3fdd18cb4f654
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # Changelog
2
+
3
+ ## v0.1.0 - 2022-03-24
4
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Stephan Tarulli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # Rails ClusterId
2
+ A Ruby on Rails plugin for using [ClusterId](https://github.com/tinychameleon/clusterid) values as primary keys.
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/rails-clusterid.svg)](https://badge.fury.io/rb/rails-clusterid)
5
+
6
+ ## What's in the box?
7
+ ✅ Simple usage documentation written to get started fast. [Check it out!](#usage)
8
+
9
+ ⚡ A pretty fast implementation of Crockford32 in pure ruby. [Check it out!](#benchmarks)
10
+
11
+ 📚 YARD generated API documentation for the library. [Check it out!](https://tinychameleon.github.io/rails-clusterid/)
12
+
13
+ 🤖 RBS types for your type checking wants. [Check it out!](./sig/rails-clusterid.rbs)
14
+
15
+ 💎 Tests against many Ruby versions. [Check it out!](#ruby-versions)
16
+
17
+ 🔒 MFA protection on all gem owners. [Check it out!](https://rubygems.org/gems/rails-clusterid)
18
+
19
+
20
+ ## Installation
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'rails-clusterid'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ $ bundle install
30
+
31
+ Or install it yourself as:
32
+
33
+ $ gem install rails-clusterid
34
+
35
+ ### Compatibility
36
+ #### Ruby Versions
37
+ This gem is tested against the following Ruby versions:
38
+
39
+ - 2.7.5
40
+ - 3.0.3
41
+ - 3.1.1
42
+
43
+ #### Rails Versions
44
+ This gem is tested against the following Ruby on Rails versions:
45
+
46
+ - 7.0.x
47
+
48
+ ## Usage
49
+ To use `ClusterId` values for all model primary keys automatically create a new initializer for your application.
50
+
51
+ ```ruby
52
+ # config/initializers/clusterid.rb
53
+
54
+ Rails.application.config.clusterid do |c|
55
+ c.add_generator(:default, ClusterId::V1::Generator.new)
56
+ end
57
+
58
+ Rails.application.config.generators do |g|
59
+ g.orm :active_record, primary_key_type: :clusterid
60
+ end
61
+ ```
62
+
63
+ The above configures the `rails-clusterid` plugin to use version 1 `ClusterId` values by default and configures ActiveRecord to use ClusterId values for primary keys.
64
+ You must add a `:default` generator, and the generator itself is configurable.
65
+ See the [ClusterId repository for information on how to configure a generator](https://github.com/tinychameleon/clusterid).
66
+
67
+ Once the initializers are complete, include the `ClusterId::Rails::Model` concern into your `ApplicationRecord`.
68
+
69
+ ```ruby
70
+ # app/models/application_record.rb
71
+
72
+ class ApplicationRecord < ActiveRecord::Base
73
+ primary_abstract_class
74
+
75
+ include ClusterId::Rails::Model
76
+ end
77
+ ```
78
+
79
+ Migrations are supported and will automatically use ClusterId values as primary keys and foreign keys.
80
+ This includes join tables.
81
+
82
+ ### Manual Usage
83
+ If you would like to selectively use `ClusterId` values across your models and migrations, just manually select the `:clusterid` type.
84
+
85
+ ```
86
+ $ rails g model person name:string --primary-key-type=clusterid
87
+ ```
88
+
89
+ You can use it within migration files as well.
90
+
91
+ ```ruby
92
+ create_table :people, id: :clusterid do |t|
93
+ t.string :name
94
+ end
95
+ ```
96
+
97
+ Since the type works with ActiveRecord you can also specify it as a foreign key.
98
+
99
+ ```ruby
100
+ create_table :hobbies, id: :clusterid do |t|
101
+ t.string :name
102
+
103
+ t.references :person, null: false, foreign_key: true, type: :clusterid
104
+ end
105
+ ```
106
+
107
+ ### ClusterId Ordering
108
+ The ClusterId values are k-sortable and will be ordered by creation time before any other part of the ID.
109
+ If you would like to read values from the ClusterId binary data stored within the database, you can deconstruct the raw data using a function.
110
+
111
+ For example, to extract the timestamp data while using PostgreSQL:
112
+ ```sql
113
+ to_timestamp(('x' || encode(substring(id for 8), 'hex'))::bit(64)::bigint / 1000.0)
114
+ ```
115
+
116
+ See the [ClusterId repository for documentation about the binary representation](https://github.com/tinychameleon/clusterid).
117
+
118
+ ### More Information
119
+ For more detailed information about the library [see the API documentation](https://tinychameleon.github.io/rails-clusterid/).
120
+
121
+
122
+ ## Contributing
123
+
124
+ ### Development
125
+ This plugin relies heavily on Docker for development; ensure it is running to use most development commands.
126
+
127
+ To get started development on this gem run the `bin/setup` command. This will build the testing Docker image and run the tests and linting commands to ensure everything is working properly.
128
+
129
+ ### Testing
130
+ Use the `bundle exec rake test` command to run unit tests. To install the gem onto your local machine for general integration testing use `bundle exec rake install`.
131
+
132
+ To test the gem against each supported version of Ruby and databases use `bin/test_versions`. This will create a Docker image for each version and run the tests and linting steps.
133
+
134
+ ### Releasing
135
+ Do the following to release a new version of this gem:
136
+
137
+ - Update the version number in [lib/rails-clusterid/version.rb](./lib/rails-clusterid/version.rb)
138
+ - Ensure necessary documentation changes are complete
139
+ - Ensure changes are in the [CHANGELOG.md](./CHANGELOG.md)
140
+ - Create the new release using `bundle exec rake release`
141
+
142
+ After this is done the following side-effects should be visible:
143
+
144
+ - A new git tag for the version number should exist
145
+ - Commits for the new version should be pushed to GitHub
146
+ - The new gem should be available on [rubygems.org](https://rubygems.org).
147
+
148
+ Finally, update the documentation hosted on GitHub Pages:
149
+
150
+ - Check-out the `gh-pages` branch
151
+ - Merge `main` into the `gh-pages` branch
152
+ - Generate the documentation with `bundle exec rake yard`
153
+ - Commit the documentation on the `gh-pages` branch
154
+ - Push the new documentation so GitHub Pages can deploy it
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId::Rails
4
+ # Contains method overrides for database migrations.
5
+ module SchemaStatements
6
+ # Overrides the default +create_join_table+ to provide support for ClusterId primary keys
7
+ # as foreign keys within join tables.
8
+ def create_join_table(table_1, table_2, column_options: {}, **options)
9
+ if ::ClusterId::Rails.clusterid_primary_keys?
10
+ column_options.reverse_merge!(type: ::ClusterId::Rails::DATA_TYPE, foreign_key: true)
11
+ end
12
+
13
+ super(table_1, table_2, column_options: column_options, **options)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId::Rails
4
+ # An +ActiveSupport::Concern+ which provides a method to access a {Config} object
5
+ # through +Rails::Configuration+.
6
+ module ApplicationConfiguration
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ def clusterid
11
+ @rails_clusterid_config ||= ::ClusterId::Rails::Config.new
12
+ yield @rails_clusterid_config if block_given?
13
+ @rails_clusterid_config
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId::Rails
4
+ # The configuration object added to the Rails configuration registry.
5
+ class Config
6
+ # Add a generator with a name to use within models.
7
+ #
8
+ # @param name [Symbol] The name to associate with the provided generator.
9
+ # @param generator [ClusterId::V1::Generator] A generator to use with models.
10
+ #
11
+ # @raise [NotAGeneratorError] when +generator+ is not one of the allowed {GENERATOR_TYPES}.
12
+ # @return [ClusterId::V1::Generator] The added generator.
13
+ def add_generator(name, generator)
14
+ raise NotAGeneratorError, "#{generator} is not a generator" unless GENERATOR_TYPES.include?(generator.class)
15
+
16
+ @generators[name] = generator
17
+ end
18
+
19
+ # Get a generator by registered name.
20
+ #
21
+ # @param name [Symbol] The registered name of a generator.
22
+ #
23
+ # @raise [KeyError] when the given +name+ was not registered via {add_generator}.
24
+ # @return [ClusterId::V1::Generator] The registered generator with the given name.
25
+ def generator(name)
26
+ @generators.fetch(name)
27
+ end
28
+
29
+ def initialize
30
+ @generators = {}
31
+ end
32
+
33
+ private
34
+
35
+ # Allowed ClusterId generator types.
36
+ GENERATOR_TYPES = [
37
+ ClusterId::V1::Generator
38
+ ].freeze
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crockford32"
4
+
5
+ module ClusterId::Rails
6
+ # An +ActiveSupport::Concern+ allowing automatic ClusterId primary keys for ActiveRecord models.
7
+ module Model
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ attribute :id, DATA_TYPE
12
+
13
+ before_create :assign_clusterid_to_id
14
+
15
+ def friendly_id
16
+ Crockford32.encode(id, length: CROCKFORD32_LENGTH)
17
+ end
18
+
19
+ def to_param
20
+ friendly_id
21
+ end
22
+
23
+ private
24
+
25
+ def clusterid_generator
26
+ ::ClusterId::Rails::DEFAULT_GENERATOR_NAME
27
+ end
28
+
29
+ def assign_clusterid_to_id
30
+ g = Rails.application.config.clusterid.generator(clusterid_generator)
31
+ self.id = g.generate.bytes.reverse
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId::Rails
4
+ # A +Rails::Railtie+ containing initialization logic for the plugin.
5
+ class Railtie < ::Rails::Railtie
6
+ initializer "rails_clusterid.column_methods" do
7
+ ::ActiveRecord::ConnectionAdapters::TableDefinition.send(:define_column_methods, DATA_TYPE)
8
+ end
9
+
10
+ initializer "rails_clusterid.native_data_types" do
11
+ adapter = ::ActiveRecord::Type.adapter_name_from(::ActiveRecord::Base)
12
+ ::ActiveRecord::Base.connection.native_database_types[DATA_TYPE] = native_type(adapter)
13
+ end
14
+
15
+ initializer "rails_clusterid.activerecord_type" do
16
+ ::ActiveRecord::Type.register(DATA_TYPE, ::ClusterId::Rails::Type, override: false)
17
+ end
18
+
19
+ initializer "rails_clusterid.activerecord_schema_statements" do
20
+ ::ActiveRecord::ConnectionAdapters::SchemaStatements.prepend(::ClusterId::Rails::SchemaStatements)
21
+ end
22
+
23
+ initializer "rails_clusterid.configuration" do
24
+ ::Rails::Application::Configuration.include(ApplicationConfiguration)
25
+ end
26
+
27
+ private
28
+
29
+ # Converts a database adapter to a hash representing the ClusterId native database type
30
+ # for ActiveRecord to query.
31
+ #
32
+ # @param adapter [Symbol] A symbol representing a database adapter.
33
+ # @return [Hash] A hash representing the ClusterId native database type.
34
+ def native_type(adapter)
35
+ case adapter
36
+ when :postgresql then {name: "bytea"}
37
+ when :mysql2 then {name: "binary", limit: BYTE_LENGTH}
38
+ when :sqlite3 then {name: "blob"}
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "clusterid"
4
+
5
+ module ClusterId::Rails
6
+ # The ActiveRecord-compatible type representing a ClusterId value optionally
7
+ # encoded using the Crockford32 algorithm.
8
+ class Type < ::ActiveRecord::Type::Binary
9
+ # Deserializes byte string ClusterId values from database reads.
10
+ #
11
+ # @param value [String, nil] The value stored in the database, possibly escaped.
12
+ # @return [String, nil] The unescaped ClusterId byte string.
13
+ def deserialize(value)
14
+ return value.to_s if value.is_a?(Data)
15
+
16
+ if database_adapter == :postgresql
17
+ ::ActiveRecord::Base.connection.unescape_bytea(value)
18
+ else
19
+ value
20
+ end
21
+ end
22
+
23
+ # Serializes byte string ClusterId values for database writes.
24
+ #
25
+ # @param value [String, nil] A ClusterId value as a byte string or the Crockford32
26
+ # encoding of that byte string.
27
+ # @return [Data, nil] The ClusterId byte string wrapped
28
+ # inside a serialization helper class.
29
+ def serialize(value)
30
+ return if value.nil?
31
+
32
+ bytes = if value.length == CROCKFORD32_LENGTH
33
+ Crockford32.decode(value, into: :string, length: BYTE_LENGTH)
34
+ else
35
+ value
36
+ end
37
+
38
+ Data.new(bytes)
39
+ end
40
+
41
+ # A helper class for serializing byte strings.
42
+ class Data < ::ActiveRecord::Type::Binary::Data; end
43
+
44
+ private
45
+
46
+ # Obtains the symbol representing the configured database adapter from
47
+ # +ActiveRecord::Base+.
48
+ def database_adapter
49
+ ::ActiveRecord::Type.adapter_name_from(::ActiveRecord::Base)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClusterId
4
+ module Rails
5
+ # The version of the gem.
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # An extension to the ClusterId[https://rubygems.org/gems/clusterid] gem which integrates
4
+ # it into Ruby on Rails.
5
+ #
6
+ # @since 0.1.0
7
+ module ClusterId
8
+ # The extension module which contains all Ruby on Rails integrations.
9
+ module Rails
10
+ # The data type symbol for use with ActiveRecord and Migrations.
11
+ DATA_TYPE = :clusterid
12
+
13
+ # The symbol representing the default ClusterId generator to use.
14
+ DEFAULT_GENERATOR_NAME = :default
15
+
16
+ # The byte length of a ClusterI value.
17
+ BYTE_LENGTH = 16
18
+
19
+ # The rune length of a ClusterId value when encoded with the Crockford32 algorithm.
20
+ CROCKFORD32_LENGTH = 26
21
+
22
+ # The base error type for {ClusterId::Rails}.
23
+ class Error < StandardError; end
24
+
25
+ # An error representing an invalid generator object is present in configuration.
26
+ class NotAGeneratorError < Error; end
27
+
28
+ # Checks whether ClusterId values are configured as the ActiveRecord primary key type.
29
+ def self.clusterid_primary_keys?
30
+ DATA_TYPE == ::Rails.application.config.generators.options.dig(:active_record, :primary_key_type)
31
+ end
32
+ end
33
+ end
34
+
35
+ require "clusterid"
36
+ require "crockford32"
37
+
38
+ require_relative "rails-clusterid/version"
39
+ require_relative "rails-clusterid/config"
40
+ require_relative "rails-clusterid/application_config"
41
+ require_relative "rails-clusterid/type"
42
+ require_relative "rails-clusterid/model"
43
+ require_relative "rails-clusterid/active_record/schema_statements"
44
+ require_relative "rails-clusterid/railtie"
@@ -0,0 +1,45 @@
1
+ module ClusterId
2
+ module Rails
3
+ DATA_TYPE: Symbol
4
+ DEFAULT_GENERATOR_NAME: Symbol
5
+ BYTE_LENGTH: Integer
6
+ CROCKFORD32_LENGTH: Integer
7
+ VERSION: String
8
+
9
+ def self.clusterid_primary_keys?: () -> bool
10
+
11
+ class Config
12
+ GENERATOR_TYPES: Array[Class]
13
+
14
+ def add_generator: (Symbol, untyped) -> untyped
15
+ def generator: (Symbol) -> untyped
16
+ end
17
+
18
+ class Railtie
19
+ end
20
+
21
+ class Type
22
+ class Data
23
+ end
24
+
25
+ def deserialize: ((Data | String)?) -> String?
26
+ def serialize: (String?) -> Data?
27
+ end
28
+
29
+ module ApplicationConfiguration
30
+ end
31
+
32
+ module Model
33
+ end
34
+
35
+ module SchemaStatements
36
+ def create_join_table: (Symbol, Symbol, column_options: Hash, **Hash) -> untyped
37
+ end
38
+
39
+ class Error < StandardError
40
+ end
41
+
42
+ class NotAGeneratorError < Error
43
+ end
44
+ end
45
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-clusterid
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephan Tarulli
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-03-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: crockford32
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: clusterid
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '7.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '7.0'
55
+ description: Use ClusterId values in Rails.
56
+ email:
57
+ - srt@tinychameleon.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - lib/rails-clusterid.rb
66
+ - lib/rails-clusterid/active_record/schema_statements.rb
67
+ - lib/rails-clusterid/application_config.rb
68
+ - lib/rails-clusterid/config.rb
69
+ - lib/rails-clusterid/model.rb
70
+ - lib/rails-clusterid/railtie.rb
71
+ - lib/rails-clusterid/type.rb
72
+ - lib/rails-clusterid/version.rb
73
+ - sig/rails-clusterid.rbs
74
+ homepage: https://github.com/tinychameleon/rails-clusterid
75
+ licenses:
76
+ - MIT
77
+ metadata:
78
+ homepage_uri: https://github.com/tinychameleon/rails-clusterid
79
+ source_code_uri: https://github.com/tinychameleon/rails-clusterid
80
+ changelog_uri: https://github.com/tinychameleon/rails-clusterid/blob/main/CHANGELOG.md
81
+ bug_tracker_uri: https://github.com/tinychameleon/rails-clusterid/issues
82
+ documentation_uri: https://tinychameleon.github.io/rails-clusterid/
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 2.7.0
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubygems_version: 3.3.3
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Use ClusterId values in Rails.
102
+ test_files: []