rails-clusterid 0.1.0

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