├── .env.development.tt ├── .env.test.tt ├── .gitignore.tt ├── .railsrc ├── Gemfile.tt ├── Procfile.tt ├── README.md ├── README.md.tt ├── app ├── jobs │ └── application_job.rb └── services │ └── application_service.rb ├── bin ├── ci.tt ├── db-migrate.tt ├── db-rollback.tt ├── release.tt ├── run.tt ├── setup.tt └── sql.tt ├── config └── initializers │ ├── lograge.rb │ ├── postgres.rb │ └── sidekiq.rb ├── lib ├── generators │ └── service │ │ ├── USAGE │ │ ├── service_generator.rb │ │ └── templates │ │ ├── service.erb │ │ └── service_test.erb ├── logging │ └── logs.rb ├── rails_ext │ └── active_record_timestamps_uses_timestamp_with_time_zone.rb ├── tasks │ └── redis.rake.tt └── templates │ └── rails │ └── job │ └── job.rb.tt ├── template.rb └── test └── lint_factories_test.rb /.env.development.tt: -------------------------------------------------------------------------------- 1 | # Access the database 2 | DATABASE_URL=postgres://postgres:postgres@db:5432/<%= app_name %>_development 3 | 4 | # Access the Redis used for Sidekiq (do not use this Redis for caching) 5 | SIDEKIQ_REDIS_URL=redis://redis:6379/1 6 | -------------------------------------------------------------------------------- /.env.test.tt: -------------------------------------------------------------------------------- 1 | # See .env.development for documentation these variables 2 | DATABASE_URL=postgres://postgres:postgres@db:5432/<%= app_name %>_test 3 | SIDEKIQ_REDIS_URL=redis://redis:6379/2 4 | -------------------------------------------------------------------------------- /.gitignore.tt: -------------------------------------------------------------------------------- 1 | # Ignore bundler config. 2 | /.bundle 3 | 4 | # Ignore all logfiles and tempfiles. 5 | /log/* 6 | /tmp/* 7 | !/log/.keep 8 | !/tmp/.keep 9 | 10 | # Ignore uploaded files in development. 11 | /storage/* 12 | !/storage/.keep 13 | 14 | # Ignore assets copied into public for local dev 15 | /public/assets 16 | 17 | # Ignore master key for decrypting credentials and more. 18 | /config/master.key 19 | 20 | # Ignore packs copied to public for local dev 21 | /public/packs 22 | # Ignore packs copied to public for local testing 23 | /public/packs-test 24 | # Ignore node_modules because WHOA 25 | /node_modules 26 | # Yarn error log is ephemeral 27 | /yarn-error.log 28 | 29 | # Ignore the .env file because it applies to both 30 | # dev and test and this is never what we want and 31 | # leads to broken hearts and empty databases 32 | .env 33 | 34 | # .env.development.local and the like are for actual secrets 35 | # we need in local dev so we don't want that checked in 36 | .env.*.local 37 | 38 | # Ignore vim session files 39 | Session.vim 40 | 41 | # Ignore vim swap files 42 | .*.sw? 43 | -------------------------------------------------------------------------------- /.railsrc: -------------------------------------------------------------------------------- 1 | --skip-turbolinks 2 | --skip-spring 3 | --skip-listen 4 | --database=postgresql 5 | -------------------------------------------------------------------------------- /Gemfile.tt: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 4 | 5 | ruby "<%= RUBY_VERSION %>" 6 | 7 | ## Rails should go at the top—it drives everything 8 | gem "rails", "~> <%= Rails.version %>" 9 | 10 | ## These gems are managed/provided by rails new 11 | gem "bootsnap"<%= gemfile_requirement("bootsnap") %>, require: false 12 | gem "pg"<%= gemfile_requirement("pg") %> 13 | gem "puma", "~> 4.1" 14 | gem "sass-rails", ">= 6" 15 | gem "webpacker"<%= gemfile_requirement("webpacker") %> 16 | 17 | ## Do Not add the following gems: 18 | # * turbolinks - we want control over how pages perform and Turbolinks is unobservable 19 | # * spring/listen - Spring causes more problems than it solves 20 | 21 | ## Our gems - keep in alphabetical order and document why each one 22 | ## is included in this project 23 | 24 | # Brakeman checks for security vulns in our code 25 | gem "brakeman" 26 | 27 | # bundler-audit enabled bundle audit 28 | # which analyzes our dependencies for 29 | # known vulnerabilities 30 | gem "bundler-audit" 31 | 32 | # Lograge manages Rails' logging so 33 | # it's a bit easier to deal with in prod 34 | gem "lograge" 35 | 36 | # Since Ruby 3, this is required but not 37 | # installed by default 38 | gem "rexml" 39 | 40 | # sidekiq is for background job processing 41 | gem "sidekiq" 42 | 43 | group :development, :test do 44 | # Configuration comes from the environment 45 | # but we use dotenv for development and test 46 | gem "dotenv-rails" 47 | 48 | # Use factories for test-specific data that you need 49 | gem "factory_bot_rails" 50 | 51 | # Foreman uses Procfile.dev to run the app locally 52 | gem "foreman" 53 | 54 | # Use Faker to generate all test data 55 | gem "faker" 56 | 57 | # This provides better test output 58 | gem "minitest-reporters" 59 | end 60 | 61 | group :test do 62 | gem "capybara", ">= 2.15" 63 | gem "selenium-webdriver" 64 | gem "webdrivers" 65 | end 66 | -------------------------------------------------------------------------------- /Procfile.tt: -------------------------------------------------------------------------------- 1 | web: bin/rails s 2 | sidekiq: bin/sidekiq 3 | release: bin/release 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rails-app-template-sustainable 2 | 3 | This is a Rails Application Template that will create a Rails app set up similarly to the one outlined in 4 | [Sustainable Web Development with Ruby on Rails](https://sustainable-rails.com). 5 | 6 | **This is for Rails 6.1 only!** 7 | 8 | ## To use 9 | 10 | ### From The Internets 11 | 12 | ``` 13 | bin/rails new my-app \ 14 | --skip-bundle \ 15 | --skip-turbolinks \ 16 | --skip-spring \ 17 | --skip-listen \ 18 | --database=postgresql \ 19 | --template=https://raw.githubusercontent.com/davetron5000/rails-app-template-sustainable/main/template.rb 20 | ``` 21 | 22 | ### Locally 23 | 24 | ``` 25 | git clone https://github.com/davetron5000/rails-app-template-sustainable 26 | rails new my-app \ 27 | --rc=rails-app-template-sustainable/.railsrc \ 28 | --template=rails-app-template-sustainable/template.rb 29 | ``` 30 | 31 | ## What you get 32 | 33 | In particular: 34 | 35 | * Gems: 36 | * Removes Gems that cause problems: 37 | - Turbolinks makes your app feel slow and broken 38 | - Spring creates an unstable development environment 39 | * Gems for better testing: 40 | - Factory Bot to manage test data 41 | - Faker to provide fake values for that data 42 | - minitest-reporters to get more useful test run output 43 | * Gems for development: 44 | - dotenv-rails to allow management of local UNIX environments 45 | - foreman to run multiple processes locally 46 | * Gems for managing security issues: 47 | - Brakeman 48 | - bundler-audit 49 | * Gems for better production behavior: 50 | - lograge for single-line logging 51 | - sidekiq for background jobs 52 | - Postgres 53 | * Dev Workflow 54 | - `bin/setup` that does a more involved setup 55 | - `bin/ci` runs all quality checks (tests, brakeman, bundle audit, yarn audit) 56 | - `bin/run` runs the app locally 57 | - `bin/sql` get a SQL prompt to your local database 58 | - `bin/db-{migrate,rollback}` - migrate and rollback both dev and test in one command 59 | - `bin/release` - Release phase script for Heroku to run migrations 60 | * Other Things 61 | - Removes `config/database.yml` and `config/secrets.yml` because your app will get all configuration from `ENV` 62 | - SQL-based schema management so you can use any feature of Postgres you like 63 | - No stylesheets or helpers generated by generators since they provide a false sense of modularity that is of 64 | zero benefit. 65 | - A simple base `ApplicationService` and a service class generator `bin/rails g service MyThing` to encourage 66 | putting code in `app/services` 67 | - All `datetime` fields in migrations uses `timestamp with time zone` which is the proper type in Postgres. 68 | - A method `confidence_check` to allow validating assumptions in tests separate from asserting code behavior. 69 | - A method `not_implemented!` to allow skipping a test you have not implemented 70 | - A test to lint all your factories 71 | 72 | ## FAQ 73 | 74 | Literally no one has asked me questions, but here are a few 75 | 76 | * Spring works though right? 77 | - It creates an unstable development environment that manifests as odd and hard-to-diagnose behavior. It will 78 | sap the time and resources of more tenured engineers on your team. It is not worth it. 79 | - This behavior is not how I would define "works". 80 | * Turbolinks makes my site feel…*slower*? 81 | - Because Turbolinks does not provide any loading animations or progress, any controller method that responds in 82 | less than 100ms (including network round trip) will make your app appear broken and slow, because it breaks the 83 | cognitive link between clicking a link and seeing something happen. A controller that takes 1 second to respond 84 | when Turbolinks is not enabled will feel faster and more reliable because the *browser* responds instantly. This should be the default behavior of your app and you should control when and how you want to optimize page performance. 85 | * But I don't want (or can't) use Postgres! 86 | - Sorry about that. This template assumes you are using Postgres. 87 | * I want to use RSpec! 88 | - I'm not stopping you from doing that. You can install RSpec and run its generator and get rid of minitest if 89 | you like. It won't make much difference to the success of your project, but it's cool. Go for it. 90 | * Some other gems should be listed here 91 | - I only want stuff you would need pretty much always, and that implies a small list of gems. I also want to 92 | be careful about gem debt, because each dependency you add is a carrying cost. 93 | -------------------------------------------------------------------------------- /README.md.tt: -------------------------------------------------------------------------------- 1 | # <%= app_name %> 2 | 3 | This app does: ???? 4 | 5 | ## Setup 6 | 7 | 1. Pull down the app from version control 8 | 2. Make sure you have Postgres and Redis running 9 | 3. `bin/setup` 10 | 11 | ## Running The App 12 | 13 | 1. `bin/run` 14 | 15 | ## Tests and CI 16 | 17 | 1. `bin/ci` contains all the tests and checks for the app 18 | 1. `tmp/test.log` will use the production logging format 19 | *not* the development one. 20 | 21 | ## Production 22 | 23 | * All runtime configuration should be supplied 24 | in the UNIX environment 25 | * Rails logging uses lograge. `bin/setup help` 26 | can tell you how to see this locally 27 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | require "logging/logs" 2 | # Do not inherit from ActiveJob. All jobs use sidekiq 3 | class ApplicationJob 4 | include Sidekiq::Worker 5 | include Logging::Logs 6 | 7 | sidekiq_options backtrace: true 8 | 9 | private 10 | 11 | def set_trace_id(trace_id_passed_to_job) 12 | trace_id_passed_to_job ||= SecureRandom.uuid 13 | Thread.current.thread_variable_set(TRACE_ID, trace_id_passed_to_job) 14 | trace_id_passed_to_job 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/services/application_service.rb: -------------------------------------------------------------------------------- 1 | require "logging/logs" 2 | class ApplicationService 3 | include Logging::Logs 4 | 5 | def trace_id(generate_if_blank: true) 6 | id = Thread.current.thread_variable_get(TRACE_ID) 7 | if id.blank? && generate_if_blank 8 | id = SecureRandom.uuid 9 | Thread.current.thread_variable_set(TRACE_ID, id) 10 | end 11 | id 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /bin/ci.tt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "[ bin/ci ] Running unit tests" 6 | bin/rails test 7 | 8 | echo "[ bin/ci ] Running system tests" 9 | bin/rails test:system 10 | 11 | echo "[ bin/ci ] Analyzing code for security vulnerabilities." 12 | echo "[ bin/ci ] Output will be in tmp/brakeman.html, which" 13 | echo "[ bin/ci ] can be opened in your browser." 14 | bundle exec brakeman -q -o tmp/brakeman.html 15 | 16 | echo "[ bin/ci ] Analyzing Ruby gems for" 17 | echo "[ bin/ci ] security vulnerabilities" 18 | bundle exec bundle audit check --update 19 | 20 | echo "[ bin/ci ] Analyzing Node modules" 21 | echo "[ bin/ci ] for security vulnerabilities" 22 | yarn audit 23 | 24 | echo "[ bin/ci ] Done" 25 | -------------------------------------------------------------------------------- /bin/db-migrate.tt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "[ bin/db-migrate ] Migrating development database" 6 | bin/rails db:migrate 7 | 8 | echo "[ bin/db-migrate ] Migrating test database" 9 | bin/rails db:migrate RAILS_ENV=test 10 | -------------------------------------------------------------------------------- /bin/db-rollback.tt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "[ bin/db-rollback ] Rolling back development database" 6 | bin/rails db:rollback 7 | 8 | echo "[ bin/db-rollback ] Rolling back test database" 9 | bin/rails db:rollback RAILS_ENV=test 10 | -------------------------------------------------------------------------------- /bin/release.tt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "[ bin/release ] Running migrations" 6 | bin/rails db:migrate 7 | -------------------------------------------------------------------------------- /bin/run.tt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "[ bin/run ] Rebuilding Procfile.dev" 6 | echo "# This is generated by bin/setup. Do not edit" > Procfile.dev 7 | echo "# Use this via bin/run" >> Procfile.dev 8 | # We must bind to 0.0.0.0 inside a 9 | # Docker container or the port won't forward 10 | echo "web: bin/rails server --binding=0.0.0.0" >> Procfile.dev 11 | grep "sidekiq:" Procfile >> Procfile.dev 12 | 13 | echo "[ bin/run ] Deleting development log" 14 | rm -f log/development.log 15 | 16 | echo "[ bin/run ] Starting forman" 17 | bin/foreman start -f Procfile.dev -p 3000 18 | -------------------------------------------------------------------------------- /bin/setup.tt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "pathname" 4 | 5 | def setup 6 | verify_local_database_config! 7 | log "💎 Installing gems" 8 | # Only do bundle install if the much-faster 9 | # bundle check indicates we need to 10 | system! "bundle check || bundle install" 11 | 12 | log "☕️ Installing Node modules" 13 | # Only do yarn install if the much-faster 14 | # yarn check indicates we need to. Note that 15 | # --check-files is needed to force Yarn to actually 16 | # examine what's in node_modules 17 | system! "bin/yarn check --check-files || bin/yarn install" 18 | 19 | log "🚧 Dropping & recreating the development database" 20 | # Note that the very first time this runs, db:reset 21 | # will fail, but this failure is fixed by 22 | # doing a db:migrate 23 | system! "bin/rails db:reset || bin/rails db:migrate" 24 | 25 | log "🧪 Dropping & recreating the test database" 26 | # Setting the RAILS_ENV explicitly to be sure 27 | # we actually reset the test database 28 | system!({ "RAILS_ENV" => "test" }, "bin/rails db:reset") 29 | 30 | log "💰 Flushing all Redis databases" 31 | system!("bin/rails redis:reset") 32 | 33 | log "🗑 Ensuring binstubs are created" 34 | system!("bundle binstubs foreman sidekiq brakeman") 35 | 36 | log "🎉 All set up! 🎉" 37 | log "" 38 | log "To see commonly-needed commands, run:" 39 | log "" 40 | log " bin/setup help" 41 | log "" 42 | end 43 | 44 | def help 45 | log "Useful commands:" 46 | log "" 47 | log " bin/run" 48 | log " # run app locally" 49 | log "" 50 | log " LOGRAGE_IN_DEVELOPMENT=true bin/run" 51 | log " # run app locally using" 52 | log " # production-like logging" 53 | log "" 54 | log " bin/sql" 55 | log " # connect to Postgres dev database" 56 | log "" 57 | log " bin/ci" 58 | log " # runs all test and checks as CI would" 59 | log "" 60 | log " bin/rails test" 61 | log " # run non-system tests" 62 | log "" 63 | log " bin/rails test:system" 64 | log " # run system tests" 65 | log "" 66 | log " bin/setup help" 67 | log " # Show this help" 68 | log "" 69 | end 70 | 71 | # start of helpers 72 | 73 | # We don't want the setup method to have 74 | # to do all this error checking, and we 75 | # also want to explicitly log what we 76 | # are executing, so we use this method 77 | # instead of Kernel#system and friends 78 | def system!(*args) 79 | log "Executing #{args}" 80 | if system(*args) 81 | log "#{args} succeeded" 82 | else 83 | log "#{args} failed" 84 | abort 85 | end 86 | end 87 | 88 | # It's helpful to know what messages came 89 | # from this script, so we'll use log 90 | # instead of puts to communicate with the user 91 | def log(message) 92 | puts "[ bin/setup ] #{message}" 93 | end 94 | 95 | def verify_local_database_config! 96 | rails_root = Pathname(__FILE__).dirname / ".." 97 | 98 | found_at_least_one_potentially_remote_database = false 99 | 100 | [ 101 | ".env.development", 102 | ".env.development.local", 103 | ".env.test", 104 | ".env.test.local", 105 | ].each do |dotenv_file| 106 | path_to_dotenv_file = rails_root / dotenv_file 107 | if File.exists?(path_to_dotenv_file) 108 | database_urls = contents = File.read(path_to_dotenv_file).split(/\n/).reject{ |line| 109 | line =~ /^#/ 110 | }.select { |line| 111 | line =~ /^[^=]*DATABASE_URL=/ 112 | } 113 | database_urls.each do |database_url| 114 | var_name, connection_string = database_url.split("=",2) 115 | if connection_string =~ /amazonaws.com/ 116 | found_at_least_one_potentially_remote_database = true 117 | log "❗️ #{var_name} in #{dotenv_file} seems to point to a non-local database" 118 | end 119 | end 120 | end 121 | end 122 | if found_at_least_one_potentially_remote_database 123 | log "‼️ There was at least one configured database that's not local" 124 | log "‼️ Running setup could blow away a real database. Please modify your .env files" 125 | exit 1 126 | end 127 | end 128 | 129 | # end of helpers 130 | 131 | if ARGV[0] == "help" 132 | help 133 | else 134 | setup 135 | end 136 | -------------------------------------------------------------------------------- /bin/sql.tt: -------------------------------------------------------------------------------- 1 | PGPASSWORD=postgres psql -U postgres -h db -p 5432 <%= app_name %>_development 2 | -------------------------------------------------------------------------------- /config/initializers/lograge.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | if !Rails.env.development? || 3 | ENV["LOGRAGE_IN_DEVELOPMENT"] == "true" 4 | config.lograge.enabled = true 5 | else 6 | config.lograge.enabled = false 7 | end 8 | end 9 | 10 | -------------------------------------------------------------------------------- /config/initializers/postgres.rb: -------------------------------------------------------------------------------- 1 | require "rails_ext/active_record_timestamps_uses_timestamp_with_time_zone" 2 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | Sidekiq.configure_server do |config| 2 | config.redis = { 3 | url: ENV.fetch("SIDEKIQ_REDIS_URL") 4 | } 5 | end 6 | 7 | Sidekiq.configure_client do |config| 8 | config.redis = { 9 | url: ENV.fetch("SIDEKIQ_REDIS_URL") 10 | } 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/service/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Create a new Service class in app/services and an associated test. 3 | 4 | Example: 5 | rails generate service Analyzer [quick_scan [detailed_analysis]] [--module Insights] 6 | 7 | This will create: 8 | app/services/insights/analyzer.rb 9 | test/services/insights/analyzer_test.rb 10 | 11 | There will be two methods, quick_scan, and detailed_analysis 12 | 13 | -------------------------------------------------------------------------------- /lib/generators/service/service_generator.rb: -------------------------------------------------------------------------------- 1 | class ServiceGenerator < Rails::Generators::NamedBase 2 | source_root File.expand_path('templates', __dir__) 3 | 4 | argument :methods, type: :array, default: [], banner: "method method" 5 | class_option :module, type: :string 6 | 7 | def create_service_file 8 | @module_name = options[:module] 9 | 10 | services_dir = "app/services" 11 | new_service_dir = services_dir + ("/#{@module_name.underscore}" if @module_name.present?).to_s 12 | service_file = new_service_dir + "/#{file_name}.rb" 13 | 14 | Dir.mkdir(services_dir) unless File.exist?(services_dir) 15 | Dir.mkdir(new_service_dir) unless File.exist?(new_service_dir) 16 | 17 | tests_dir = "test/services" 18 | new_test_dir = tests_dir + ("/#{@module_name.underscore}" if @module_name.present?).to_s 19 | test_file = new_test_dir + "/#{file_name}_test.rb" 20 | 21 | Dir.mkdir(tests_dir) unless File.exist?(tests_dir) 22 | Dir.mkdir(new_test_dir) unless File.exist?(new_test_dir) 23 | 24 | template "service.erb", service_file 25 | template "service_test.erb", test_file 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/generators/service/templates/service.erb: -------------------------------------------------------------------------------- 1 | <% if @module_name.present? %> 2 | module <%= @module_name.camelize %> 3 | end 4 | <% end %> 5 | class <% if @module_name.present? %><%= @module_name.camelize %>::<% end %><%= class_name %> < ApplicationService 6 | 7 | <% for method in methods %> 8 | def <%= method %>(args,go,here) 9 | raise "not implemented" 10 | end 11 | <% end %> 12 | 13 | private 14 | 15 | class Result 16 | # Create your rich result class here 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/generators/service/templates/service_test.erb: -------------------------------------------------------------------------------- 1 | <% if @module_name.present? %> 2 | module <%= @module_name.camelize %> 3 | <% end %> 4 | class <% if @module_name.present? %><%= @module_name.camelize %>::<% end %><%= class_name %>Test < ActiveSupport::TestCase 5 | 6 | setup do 7 | @<%= class_name.underscore %> = <%= class_name %>.new 8 | end 9 | 10 | test "it does a thing" do 11 | not_implemented! 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /lib/logging/logs.rb: -------------------------------------------------------------------------------- 1 | module Logging 2 | module Logs 3 | TRACE_ID = "trace_id" 4 | def log(method, message) 5 | trace_id = Thread.current.thread_variable_get(TRACE_ID) 6 | Rails.logger.info("[Logging::Logs]#{format_trace_id(trace_id)}#{self.class}##{method}: #{message}") 7 | end 8 | 9 | private 10 | def format_trace_id(trace_id) 11 | return nil if trace_id.blank? 12 | " trace_id:#{trace_id} " 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/rails_ext/active_record_timestamps_uses_timestamp_with_time_zone.rb: -------------------------------------------------------------------------------- 1 | require "active_record/connection_adapters/postgresql_adapter.rb" 2 | 3 | ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::NATIVE_DATABASE_TYPES[:datetime] = { 4 | name: "timestamptz" 5 | } 6 | -------------------------------------------------------------------------------- /lib/tasks/redis.rake.tt: -------------------------------------------------------------------------------- 1 | namespace :redis do 2 | desc "Clear out the redis database entirely" 3 | task :reset => :environment do 4 | if Rails.env.development? 5 | Sidekiq.redis do |redis| 6 | redis.flushall 7 | end 8 | puts "[ redis:reset ] All redis dbs flushed" 9 | else 10 | puts "!!!! You cannot redis:reset outside of development !!!" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/templates/rails/job/job.rb.tt: -------------------------------------------------------------------------------- 1 | <% module_namespacing do -%> 2 | class <%= class_name %>Job < ApplicationJob 3 | def perform(some_arg, trace_id = nil) 4 | log :perform, "(#{some_arg})" 5 | 6 | set_trace_id(trace_id) 7 | 8 | raise "Job not implemented" 9 | 10 | end 11 | end 12 | <% end -%> 13 | -------------------------------------------------------------------------------- /template.rb: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | require "shellwords" 3 | require "fileutils" 4 | require "tmpdir" 5 | 6 | RAILS_REQUIREMENT = "~> 6.1.0".freeze 7 | 8 | def apply_template! 9 | 10 | add_template_repository_to_source_path 11 | 12 | assert_minimum_rails_version 13 | assert_valid_options 14 | assert_postgres 15 | 16 | template "Gemfile.tt", force: true 17 | 18 | template "README.md.tt", force: true 19 | remove_file "README.rdoc" 20 | 21 | template ".env.development.tt" 22 | template ".env.test.tt" 23 | template ".gitignore.tt", force: true 24 | 25 | template "bin/setup.tt", force: true 26 | template "bin/ci.tt" 27 | template "bin/run.tt" 28 | template "bin/sql.tt" 29 | template "bin/db-migrate.tt" 30 | template "bin/db-rollback.tt" 31 | template "bin/release.tt" 32 | template "Procfile.tt" 33 | 34 | remove_file "config/database.yml" 35 | remove_file "config/secrets.yml" 36 | 37 | template "lib/tasks/redis.rake.tt" 38 | 39 | insert_into_file "config/application.rb", 40 | before: /^ end\s*$/ do 41 | [ 42 | " # We will use the fully-armed and operational power of Postgres", 43 | " # and that means using SQL-based structure.", 44 | " config.active_record.schema_format = :sql", 45 | "", 46 | " config.generators do |g|", 47 | " # We don't want per-resource stylesheets since", 48 | " # that is not how stylesheets work.", 49 | " g.stylesheets false", 50 | "", 51 | " # We don't want per-resource helpers because", 52 | " # helpers are global anyway and we don't want", 53 | " # a ton of them.", 54 | " g.helper false", 55 | " end", 56 | ].join("\n") + "\n" 57 | end 58 | 59 | remove_file "db/schema.rb" if File.exist?("db/schema.rb") 60 | 61 | copy_file "lib/generators/service/USAGE" 62 | copy_file "lib/generators/service/service_generator.rb" 63 | copy_file "lib/generators/service/templates/service.erb" 64 | copy_file "lib/generators/service/templates/service_test.erb" 65 | copy_file "lib/logging/logs.rb" 66 | copy_file "lib/rails_ext/active_record_timestamps_uses_timestamp_with_time_zone.rb" 67 | copy_file "lib/templates/rails/job/job.rb.tt" 68 | copy_file "config/initializers/postgres.rb" 69 | copy_file "config/initializers/sidekiq.rb" 70 | 71 | insert_into_file "config/routes.rb", 72 | "require \"sidekiq/web\"\n\n", 73 | before: "Rails.application.routes.draw do" 74 | 75 | insert_into_file "config/routes.rb", before: /^end\s*$/ do 76 | [ 77 | "", 78 | " if !Rails.env.development?", 79 | " Sidekiq::Web.use Rack::Auth::Basic do |username, password|", 80 | " username == ENV.fetch(\"SIDEKIQ_WEB_USER\") &&", 81 | " password == ENV.fetch(\"SIDEKIQ_WEB_PASSWORD\")", 82 | " end", 83 | " end", 84 | " mount Sidekiq::Web => \"/sidekiq\"", 85 | ].join("\n") 86 | end 87 | 88 | copy_file "app/jobs/application_job.rb", force: true 89 | copy_file "app/services/application_service.rb", force: true 90 | 91 | insert_into_file "test/test_helper.rb", after: "require \"rails/test_help\"" do 92 | [ 93 | "", 94 | "require \"minitest/autorun\"", 95 | "require \"minitest/reporters\"", 96 | "", 97 | "Minitest::Reporters.use!", 98 | "unless ENV[\"MINITEST_REPORTER\"]", 99 | " Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new", 100 | "end", 101 | ].join("\n") 102 | end 103 | gsub_file "test/test_helper.rb", 104 | "# Add more helper methods to be used by all tests here..." do 105 | [ 106 | "include FactoryBot::Syntax::Methods", 107 | "", 108 | " # Used to indicate assertions that confidence check test", 109 | " # set up conditions", 110 | " def confidence_check(context=nil, &block)", 111 | " block.()", 112 | " rescue Exception", 113 | " puts context.inspect", 114 | " raise", 115 | " end", 116 | "", 117 | " # Used inside a test to indicate we haven't implemented it yet", 118 | " def not_implemented!", 119 | " skip(\"not implemented yet\")", 120 | " end", 121 | ].join("\n") 122 | end 123 | 124 | copy_file "test/lint_factories_test.rb" 125 | end 126 | 127 | def run_with_clean_bundler_env(cmd) 128 | success = if defined?(Bundler) 129 | Bundler.with_clean_env { run(cmd) } 130 | else 131 | run(cmd) 132 | end 133 | unless success 134 | puts "Command failed, exiting: #{cmd}" 135 | exit(1) 136 | end 137 | end 138 | 139 | def assert_minimum_rails_version 140 | requirement = Gem::Requirement.new(RAILS_REQUIREMENT) 141 | rails_version = Gem::Version.new(Rails::VERSION::STRING) 142 | return if requirement.satisfied_by?(rails_version) 143 | 144 | fail "This template reqires Rails #{RAILS_REQUIREMENT}, but you are using #{rails_version}" 145 | end 146 | 147 | def assert_postgres 148 | return if IO.read("Gemfile") =~ /^\s*gem ['"]pg['"]/ 149 | fail Rails::Generators::Error, 150 | "This template requires PostgreSQL, "\ 151 | "but the pg gem isn’t present in your Gemfile. Use -d postgresql to rails new" 152 | end 153 | 154 | def assert_valid_options 155 | valid_options = { 156 | skip_gemfile: false, 157 | skip_git: false, 158 | skip_system_test: false, 159 | skip_test: false, 160 | skip_test_unit: false, 161 | edge: false, 162 | skip_listen: true, 163 | skip_spring: true, 164 | skip_turbolinks: true, 165 | } 166 | valid_options.each do |key, expected| 167 | if expected == false 168 | next unless options.key?(key) 169 | end 170 | actual = options[key] 171 | unless actual == expected 172 | fail Rails::Generators::Error, "Unsupported option: #{key}=#{actual}\n\nYou must run with --skip-listen --skip-spring --skip-turbolinks" 173 | end 174 | end 175 | end 176 | 177 | # Add this template directory to source_paths so that Thor actions like 178 | # copy_file and template resolve against our source files. If this file was 179 | # invoked remotely via HTTP, that means the files are not present locally. 180 | # In that case, use `git clone` to download them to a local temporary dir. 181 | def add_template_repository_to_source_path 182 | if __FILE__ =~ %r{\Ahttps?://} 183 | source_paths.unshift(tempdir = Dir.mktmpdir("rails-app-template-sustainable-")) 184 | at_exit { FileUtils.remove_entry(tempdir) } 185 | git clone: [ 186 | "--quiet", 187 | "https://github.com/davetron5000/rails-app-template-sustainable.git", 188 | tempdir 189 | ].map(&:shellescape).join(" ") 190 | 191 | if (branch = __FILE__[%r{rails-app-template-sustainable/(.+)/template.rb}, 1]) 192 | Dir.chdir(tempdir) { git checkout: branch } 193 | end 194 | else 195 | source_paths.unshift(File.dirname(__FILE__)) 196 | end 197 | end 198 | 199 | def gemfile_requirement(name) 200 | @original_gemfile ||= IO.read("Gemfile") 201 | req = @original_gemfile[/gem\s+['"]#{name}['"]\s*(,[><~= \t\d\.\w'"]*)?.*$/, 1] 202 | req && req.gsub("'", %(")).strip.sub(/^,\s*"/, ', "') 203 | end 204 | 205 | require "thor" 206 | class Thor::Actions::InjectIntoFile 207 | 208 | protected 209 | 210 | # Copied from lib/thor/actions/inject_into_file.rb so I can 211 | # raise if the regexp fails 212 | def replace!(regexp, string, force) 213 | return if pretend? 214 | content = File.read(destination) 215 | if force || !content.include?(replacement) 216 | # BEGIN CHANGE 217 | result = content.gsub!(regexp, string) 218 | if result.nil? 219 | raise "Regexp didn't match: #{regexp}:\n#{string}" 220 | end 221 | # END CHANGE 222 | # ORIGINAL CODE 223 | # content.gsub!(regexp, string) 224 | # END ORIGINAL CODE 225 | File.open(destination, "wb") { |file| file.write(content) } 226 | end 227 | end 228 | end 229 | 230 | module Thor::Actions 231 | # Copied from lib/thor/actions/file_manipulation.rb 232 | def gsub_file(path, flag, *args, &block) 233 | return unless behavior == :invoke 234 | config = args.last.is_a?(Hash) ? args.pop : {} 235 | 236 | path = File.expand_path(path, destination_root) 237 | say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true) 238 | 239 | unless options[:pretend] 240 | content = File.binread(path) 241 | # BEGIN CHANGE 242 | result = content.gsub!(flag, *args, &block) 243 | if result.nil? 244 | raise "Regexp didn't match #{flag}:\n#{content}" 245 | end 246 | # END CHANGE 247 | # ORIGINAL CODE 248 | # content.gsub!(flag, *args, &block) 249 | # END ORIGINAL CODE 250 | File.open(path, "wb") { |file| file.write(content) } 251 | end 252 | end 253 | end 254 | 255 | apply_template! 256 | -------------------------------------------------------------------------------- /test/lint_factories_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class LintFactoriesTest < ActiveSupport::TestCase 4 | test "all factories can be created" do 5 | FactoryBot.lint traits: true 6 | end 7 | end 8 | --------------------------------------------------------------------------------