Quickstarter with sensibles
Used 5 times
M
Magnous
Usage
Run this command in your Rails app directory in the terminal:
rails app:template LOCATION="https://railsbytes.com/script/VMys0d"
Template Source
Review the code before running this template on your machine.
say_status :info, "Hey! Let's start with the Sensible Rails installation"
require 'uri'
require 'open-uri'
require 'fileutils'
# DEV_MODE=true rails new rvmd --database=postgresql --css=tailwind --skip-javascript --skip-sprockets --template="rails_prototype/template.rb" --skip-kamal
# TODO: add business specs foe existing operations
def source_paths
[File.expand_path(__dir__)]
end
# plugins = []
# base_configs = []
# extensions = []
# gems = []
#
# specs = []
# deps = []
if Rails.version < '8.0.0'
fail 'please use rails 8.0.0 or above'
end
def in_root(&block)
inside Rails.application.root, &block
end
def do_bundle
# Custom bundle command ensures dependencies are correctly installed
Bundler.with_unbundled_env { run "bundle install" }
end
def bundle_add(*packages)
packages.each do |package|
say_status :info, "Adding #{package} to Gemfile"
run "bundle add #{package}"
end
end
def yarn(*packages)
run("yarn add #{packages.join(" ")}")
end
def ruby_version
in_root do
if File.file?("Gemfile.lock")
bundler_parser = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock"))
specs = bundler_parser.specs.map(&:name)
locked_version = bundler_parser.ruby_version.match(/(\d+\.\d+\.\d+)/)&.[](1) if bundler_parser.ruby_version
# ruby_version = Gem::Version.new(locked_version).segments[0..1].join(".") if locked_version
# maybe_ruby_version = bundler_parser.ruby_version&.match(/ruby (\d+\.\d+\.\d+)./i)&.[](1)
Gem::Version.new(locked_version).segments[0..1].join(".")
end
end
end
def specs
in_root do
if File.file?("Gemfile.lock")
bundler_parser = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock"))
bundler_parser.specs.map(&:name)
end
end
end
def deps
in_root do
if File.file?("Gemfile")
bundler_parser = Bundler::Dsl.new
bundler_parser.eval_gemfile("Gemfile")
bundler_parser.dependencies.map(&:name)
end
end
end
def has_gem?(name)
deps.include?(name) || specs.include?(name)
end
def needs_gem?(name)
!has_gem?(name)
end
def has_rails
((specs | deps) & %w[activerecord actionpack rails]).any?
end
def has_rspec
in_root do
((specs | deps) & %w[rspec-core]).any? && File.directory?("spec")
end
end
def download_file(from_path, to_path = from_path)
if ENV['DEV_MODE']
# for local development and upgrades
copy_file from_path, to_path
else
base_url = 'https://raw.githubusercontent.com/OrestF/rails_prototype/rails_api'
get([base_url, from_path].join('/'), to_path)
end
end
def gem_add(gem_name, **args)
if Gem.loaded_specs.key?(gem_name)
say_status :info, "Ruby gem #{gem_name} already loaded"
else
gem gem_name, **args
end
end
def app_name
Rails.application.class.name.partition('::').first.parameterize
end
def root_dir
Dir.pwd
end
def name
root_dir.rpartition("/").last
end
def human_name
name.split(/[-_]/).map(&:capitalize).join(" ")
end
def find_and_replace_in_file(file_name, old_content, new_content)
text = File.read(file_name)
new_contents = text.gsub(old_content, new_content)
File.open(file_name, 'w') { |file| file.write new_contents }
end
## Gathered Facts
def say_facts
say "Gathering facts..."
say "=================="
say "Ruby version: #{ruby_version}"
say "Gemspecs: #{specs.join(", ")}"
say "=================="
say "App name: #{app_name}"
say "Human name: #{human_name}"
say "Root dir: #{root_dir}"
say "Original Name: #{name}"
say "=================="
say "Git Base Config:"
say "Git user name: #{git config: 'get user.name'}"
say "Git user email: #{git config: 'get user.email'}"
end
def git_setup
if File.exist?('/.dockerenv')
git_username = git config: 'get user.email' || ask("What's your git username?")
git_email = git config: 'get user.email' || ask("What's your git email?")
say_status :info, "Setting up git..."
say_status :info, "Setup git user info..."
git config: "--global user.email '#{git_email}'"
git config: "--global user.name '#{git_username}'"
say_status :info, "Setup git global config..."
say_status :info, "Setup the editor to vim..."
git config: "--global core.editor 'vim -w'"
say_status :info, "Set the global gitignore..."
git config: "--global core.excludesfile '~/.gitignore_global'"
say_status :info, "Set the default branch..."
git config: "--global init.defaultBranch main"
say_status :info, "Set the global pull options..."
git config: "--global pull.rebase true"
say_status :info, "Set the pull ff strategy..."
git config: "--global pull.ff only"
say_status :info, "Set the safe directories..."
git config: "--global safe.directory /project"
git config: "--global safe.directory /bench"
end
end
def install_gems
gem "active_hash" if needs_gem?("active_hash")
gem "attr_json" if needs_gem?("attr_json")
gem "flexirest", "~> 1.12.5" if needs_gem?("flexirest")
gem "spyke" if needs_gem?("spyke")
gem "shale", "~> 1.2" if needs_gem?("shale")
gem "mission_control-jobs" if needs_gem?("mission_control-jobs")
gem "multi_json" if needs_gem?("multi_json")
gem "motor-admin" if needs_gem?("motor-admin")
end
def add_logging
create_file "config/initializers/logging.rb", <<~'TCODE'
# frozen_string_literal: true
# Custom log formatter that adds timestamp and origin IP
# Extends ActiveSupport::Logger::SimpleFormatter and adds tagged logging support
class CustomLogFormatter < ActiveSupport::Logger::SimpleFormatter
# Add tagged logging support for compatibility with ActiveJob
include ActiveSupport::TaggedLogging::Formatter
def call(severity, timestamp, progname, msg)
# Get the current request context if available (thread-local storage)
request_ip = Thread.current[:request_ip] || '-'
formatted_datetime = timestamp.strftime('%Y-%m-%d %H:%M:%S.%3N')
# Include tags if present (for ActiveJob compatibility)
tag_string = current_tags.any? ? "#{tags_text} " : ""
"[#{formatted_datetime}] [#{request_ip}] #{severity} -- #{tag_string}: #{msg}\n"
end
end
# Middleware to capture request IP in thread-local storage
class RequestIpLogger
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
# Capture origin IP (handles proxies via X-Forwarded-For)
Thread.current[:request_ip] = request.remote_ip
@app.call(env)
ensure
# Clean up after request
Thread.current[:request_ip] = nil
end
end
# Apply formatter to Rails logger with tagged logging support
if Rails.logger.respond_to?(:formatter=)
# Ensure the logger supports tagged logging
unless Rails.logger.is_a?(ActiveSupport::TaggedLogging)
Rails.logger = ActiveSupport::TaggedLogging.new(Rails.logger)
end
Rails.logger.formatter = CustomLogFormatter.new
end
# Insert middleware to capture request IP
Rails.application.config.middleware.insert_before(
Rails::Rack::Logger,
RequestIpLogger
)
TCODE
end
def add_flexirest
create_file "config/initializers/faraday_config.rb", <<~'TCODE'
Flexirest::Base.faraday_config do |faraday|
faraday.adapter(:net_http)
faraday.options.timeout = 10
faraday.ssl.verify = false
faraday.headers['User-Agent'] = "Flexirest/#{Flexirest::VERSION}"
end
# Flexirest::Base.cache_store = :redis_store, { url: "redis://redis:6379/1" }
# Flexirest::Base.cache_store = :file_store, "tmp/cache"
# Flexirest::Base.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }
TCODE
end
def add_mappers
create_file "config/initializers/shale.rb", <<~'TCODE'
require 'shale/adapter/csv'
Shale.csv_adapter = Shale::Adapter::CSV
TCODE
create_file "config/initializers/spyke.rb", <<~'TCODE'
# config/initializers/spyke.rb
class JSONParser < Faraday::Middleware
def on_complete(env)
json = MultiJson.load(env.body, symbolize_keys: true)
env.body = {
data: json[:data],
metadata: json[:meta] || json[:metadata] || {},
errors: json[:message] || json[:errors] || {}
}
rescue MultiJson::ParseError => exception
env.body = { errors: { base: [error: exception.message] } }
end
end
TCODE
end
def add_core
add_logging
add_flexirest
add_mappers
end
def setup_motor_admin
generate 'motor:install'
rails_command 'db:migrate'
puts <<-TEXT
IMPORTANT!
In order to secure MotorAdmin, you need to specify environment variables on your servers:
MOTOR_AUTH_USERNAME
MOTOR_AUTH_PASSWORD
TEXT
end
def add_motor_admin_env
append_to_file '.env.sample', 'MOTOR_ACTIVE_STORAGE_DIRECT_UPLOADS_ENABLED=true'
end
def enable_active_storage
rails_command 'active_storage:install'
rails_command 'db:migrate'
end
def add_extras
enable_active_storage
add_motor_admin_env
setup_motor_admin
end
say "Hey from the included template!"
say_facts
git_setup
install_gems
after_bundle do
add_core
add_extras
end