sensible-rails

Public
Quickstarter with sensibles
Used 5 times
Created by
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



Comments

Sign up or Login to leave a comment.