From b2c285db15e9fcc469c278e1519991ea0753fe0c Mon Sep 17 00:00:00 2001 From: Hasarinda Thenuwara Date: Mon, 22 Jun 2026 16:03:21 +0930 Subject: [PATCH] Install solid cable using the primary database --- Gemfile | 1 + Gemfile.lock | 22 +++++ .../thermite/install/solid_cable/USAGE | 16 +++ .../solid_cable/solid_cable_generator.rb | 98 +++++++++++++++++++ .../templates/add_solid_cable.rb.tt | 7 ++ .../solid_cable/templates/config/cable.yml.tt | 16 +++ lib/thermite/tasks.rb | 5 + .../install/solid_cable_generator_spec.rb | 31 ++++++ spec/support/generator_helpers.rb | 1 + 9 files changed, 197 insertions(+) create mode 100644 lib/generators/thermite/install/solid_cable/USAGE create mode 100644 lib/generators/thermite/install/solid_cable/solid_cable_generator.rb create mode 100644 lib/generators/thermite/install/solid_cable/templates/add_solid_cable.rb.tt create mode 100644 lib/generators/thermite/install/solid_cable/templates/config/cable.yml.tt create mode 100644 spec/generators/thermite/install/solid_cable_generator_spec.rb diff --git a/Gemfile b/Gemfile index afcedb4..b5e4a07 100644 --- a/Gemfile +++ b/Gemfile @@ -8,4 +8,5 @@ gem "activestorage" gem "rake" gem "rspec" gem "rubocop-katalyst" +gem "solid_cable" gem "solid_queue" diff --git a/Gemfile.lock b/Gemfile.lock index bffe3bc..8fd50f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,6 +8,12 @@ PATH GEM remote: https://rubygems.org/ specs: + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) actionpack (8.1.3) actionview (= 8.1.3) activesupport (= 8.1.3) @@ -90,6 +96,7 @@ GEM minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) + nio4r (2.7.5) nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.4-aarch64-linux-musl) @@ -218,6 +225,11 @@ GEM rubocop-rspec (~> 3.5) ruby-progressbar (1.13.0) securerandom (0.4.1) + solid_cable (4.0.0) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) solid_queue (1.4.0) activejob (>= 7.1) activerecord (>= 7.1) @@ -236,6 +248,10 @@ GEM unicode-emoji (4.2.0) uri (1.1.1) useragent (0.16.11) + websocket-driver (0.8.1) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) zeitwerk (2.8.2) PLATFORMS @@ -254,9 +270,11 @@ DEPENDENCIES rake rspec rubocop-katalyst + solid_cable solid_queue CHECKSUMS + actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0 actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3 actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3 @@ -291,6 +309,7 @@ CHECKSUMS loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 + nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca @@ -336,6 +355,7 @@ CHECKSUMS rubocop-rspec_rails (2.32.0) sha256=4a0d641c72f6ebb957534f539d9d0a62c47abd8ce0d0aeee1ef4701e892a9100 ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1 + solid_cable (4.0.0) sha256=8379680ef6bf36e195eb876a6306ea290f87d5fa10bc4a757bc2a918f83229b5 solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 @@ -346,6 +366,8 @@ CHECKSUMS unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 + websocket-driver (0.8.1) sha256=5ab238238ce230e5d4b262d2be39624c867914eab99171dc4952b58b577c2d96 + websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 BUNDLED WITH diff --git a/lib/generators/thermite/install/solid_cable/USAGE b/lib/generators/thermite/install/solid_cable/USAGE new file mode 100644 index 0000000..c54bd8d --- /dev/null +++ b/lib/generators/thermite/install/solid_cable/USAGE @@ -0,0 +1,16 @@ +Description: + Installs Solid Cable using a single, shared database (Katalyst's preferred setup). + + Unlike `solid_cable:install`, this does not provision a separate cable + database. The solid_cable tables are added to the primary database via a + normal migration in db/migrate, and config/cable.yml uses the solid_cable + adapter without a `connects_to` configuration. + + Requires the solid_cable gem to be present in the host application. + +Example: + bin/rails thermite:install:solid_cable + + This will: + Create config/cable.yml + Create a db/migrate migration for the solid_cable tables diff --git a/lib/generators/thermite/install/solid_cable/solid_cable_generator.rb b/lib/generators/thermite/install/solid_cable/solid_cable_generator.rb new file mode 100644 index 0000000..3aece57 --- /dev/null +++ b/lib/generators/thermite/install/solid_cable/solid_cable_generator.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "rails" +require "rails/generators" +require "rails/generators/active_record/migration" + +require "active_record" +require "yaml" + +# For a single-database install, solid cable's documented setup is to copy the contents +# of db/cable_schema.rb into a normal migration, delete db/cable_schema.rb, +# remove connects_to from config/cable.yml, then run db:migrate. +# Thermite follows that path by generating the migration directly. +# +# solid_cable is an optional dependency: it is required lazily in #verify_solid_cable! +# so the generator still loads (and lists) when solid_cable is absent. +module Thermite + module Install + class SolidCableGenerator < Rails::Generators::Base + # Provides migration_template, next_migration_number (timestamped) and the + # CreateMigration action that makes re-runs safe: identical content is + # skipped, a same-named migration with different content raises a conflict. + include ActiveRecord::Generators::Migration + + # Reuse solid_cable's own templates. Resolved lazily so the class loads + # without solid_cable present; populated once #verify_solid_cable! requires it. + def self.source_root + ::SolidCable::InstallGenerator.source_root if defined?(::SolidCable::InstallGenerator) + end + + # Search our own templates (the migration) first, then solid_cable's + # (cable.yml). + def self.source_paths + [File.expand_path("templates", __dir__), source_root].compact + end + + # Our source_root points into solid_cable, so the inherited USAGE lookup + # would resolve to solid_cable's USAGE. Point it at ours instead. + def self.usage_path + File.expand_path("USAGE", __dir__) + end + + # Fail early with a helpful message when solid_cable isn't available. + def verify_solid_cable! + require "solid_cable/version" + require "generators/solid_cable/install/install_generator" + rescue LoadError + raise Thor::Error, <<~MSG.strip + thermite:install:solid_cable requires the solid_cable gem, which is not available. + Add it to your Gemfile and run `bundle install`: + + gem "solid_cable" + MSG + end + + # Based on SolidCable::InstallGenerator#copy_files but writing schema to a migration instead + # @see https://github.com/rails/solid_cable#single-database-configuration + def copy_files + template "config/cable.yml" + migration_template "add_solid_cable.rb", "db/migrate/add_solid_cable.rb", skip: true + end + + private + + # The application's extra environments, derived from config/environments/*.rb so + # we don't hard-code names like "staging" / "uat". + def extra_environments + environments + .reject { |pathname| local?(pathname) || production?(pathname) } + .map { |path| path.basename(".rb").to_s } + end + + def environments + Pathname(destination_root).join("config/environments").glob("*.rb") + end + + def local?(pathname) + %w[development test].include?(pathname.basename(".rb").to_s) + end + + def production?(pathname) + pathname.basename(".rb").to_s == "production" + end + + def solid_cable_schema_body + schema = File.read(File.join(self.class.source_root, "db/cable_schema.rb")) + + schema + # Keep only the schema block body. + .sub(%r{\A.*ActiveRecord::Schema[^\n]+do\n}, "") + .sub(/\nend\n?\z/, "") + .lines + .map { |line| line == "\n" ? line : " #{line}" } + .join + end + end + end +end diff --git a/lib/generators/thermite/install/solid_cable/templates/add_solid_cable.rb.tt b/lib/generators/thermite/install/solid_cable/templates/add_solid_cable.rb.tt new file mode 100644 index 0000000..6d75494 --- /dev/null +++ b/lib/generators/thermite/install/solid_cable/templates/add_solid_cable.rb.tt @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] + def change +<%= solid_cable_schema_body.chomp %> + end +end diff --git a/lib/generators/thermite/install/solid_cable/templates/config/cable.yml.tt b/lib/generators/thermite/install/solid_cable/templates/config/cable.yml.tt new file mode 100644 index 0000000..03c623c --- /dev/null +++ b/lib/generators/thermite/install/solid_cable/templates/config/cable.yml.tt @@ -0,0 +1,16 @@ +development: + adapter: async + +test: + adapter: test + +production: &production + adapter: solid_cable + polling_interval: 0.1.seconds + message_retention: 1.day + +<% extra_environments.each do |environment| -%> + +<%= environment %>: + <<: *production +<% end -%> diff --git a/lib/thermite/tasks.rb b/lib/thermite/tasks.rb index 31d484a..a0abe84 100644 --- a/lib/thermite/tasks.rb +++ b/lib/thermite/tasks.rb @@ -11,5 +11,10 @@ task :active_storage do Rails::Command.invoke :generate, ["thermite:install:active_storage"] end + + desc "Install Solid Cable" + task :solid_cable do + Rails::Command.invoke :generate, ["thermite:install:solid_cable"] + end end end diff --git a/spec/generators/thermite/install/solid_cable_generator_spec.rb b/spec/generators/thermite/install/solid_cable_generator_spec.rb new file mode 100644 index 0000000..c225ea9 --- /dev/null +++ b/spec/generators/thermite/install/solid_cable_generator_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "rails_helper" + +require "generators/thermite/install/solid_cable/solid_cable_generator" + +RSpec.describe Thermite::Install::SolidCableGenerator do + it "completes successfully" do + expect { run_generator }.not_to raise_error + end + + it "writes cable config and migration" do + run_generator + + assert_file "config/cable.yml" + assert_migration "db/migrate/add_solid_cable.rb" + end + + it "includes non-standard environments like staging in the cable config" do + run_generator + + assert_file "config/cable.yml", /^staging:/ + end + + it "raises a helpful error when solid_cable is unavailable" do + allow(generator).to receive(:require).and_raise(LoadError) + + expect { generator.verify_solid_cable! } + .to raise_error(Thor::Error, /requires the solid_cable gem/) + end +end diff --git a/spec/support/generator_helpers.rb b/spec/support/generator_helpers.rb index 448ef63..641afe4 100644 --- a/spec/support/generator_helpers.rb +++ b/spec/support/generator_helpers.rb @@ -2,6 +2,7 @@ require "rails/generators/testing/behavior" require "rails/generators/testing/assertions" +require "minitest" require "minitest/assertions" require "fileutils"