Renaming Rails Models: A Do-Over Approach

JUL 21, 2016 • Written by Don Denoncourt

In one of my current Rails projects, we had two classes with the names of Incident and Event. But the application’s target industry uses the term events for what we were calling incidents and the thing we named event was more verbosely called a Scheduled Event. Our client has the same philosophy Corgibytes has when it comes to naming: names are important and naming is a process. So, even though we have lots of programming tasks on this project, the ticket for renaming these two models was moved to the top of our Kanban’s ready column.

The process of renaming models in Rails can be very error prone. To just start renaming files and changing class names and search-replace variable names is fraught with peril – so I figured having the ability to repeat the process, in essence fix my scripting mistakes and “do-over,” was important.

To enable do-over, I first created a shell script that started with the following two lines:

git clean -df
git checkout -- .

The above git commands removed all local changes and reset my file system to the currently checked out branch. I then proceeded to update the script with the steps necessary to name the models. As I ran the script, I ran the project’s unit tests in parallel. As the unit tests, which were run in the shell script, discovered problems, I’d modify and rerun the script. I probably did this process a dozen times before I felt comfortable committing the code, creating a pull request, and asking a team member to review and then merge the code.

The final steps of the rename process were:

  1. Build and run a migration that renames the tables and columns.
  2. Rename directories and then files.
  3. Replace strings in code that match the Pascal-cased version of the class name (e.g. Event to ScheduledEvent).
  4. Replace strings in code that match the underscored version class name (e.g. event to scheduled_event)

Creating the migration to rename the models and columns was easy. Note that, when you rename a table, the Rails migration tool takes care of renaming indexes as well. The following is the resulting migration:

class RenameIncidentEvents < ActiveRecord::Migration
 def change
   rename_table :events, :scheduled_events
   rename_table :incidents, :events
   rename_table :scene_events, :scene_scheduled_events
   rename_table :scene_incidents, :scene_events
   rename_column :origins, :incident_id, :event_id
   rename_column :scene_events, :incident_id, :event_id
   rename_column :scene_scheduled_events, :event_id, :scheduled_event_id
 end
end

Note that there were two tables that I had missed in my initial assessment. These were has_many association tables. My do-over script made it easy for me to, 1) find the problem through unit tests, and 2) reset everything and run the whole renaming process again. Understand that this process assumes a decent set of unit tests. If the application doesn’t have unit tests, either write them or stop enhancing the application.

To discover what files and directories would require renaming, I used the Linux find command:

find . -name '*event*'

I pasted the output of the find command into my do-over shell script and then changed the file list into Linux mv command rename statements.

After processing the directory and file renames, my do-over script moved on to replacing occurrences of the Pascal-cased class name (e.g. Event to Gremlin) and underscored class names (e.g. event to gremlin.) This task was very problematic. My solution was to do the renaming steps in three phases:

1) Temporarily rename Event to Gremlin and event to gremlin 2) Rename Incident to Event and incident to event 3) Rename Gremlin to the final desired name of ScheduledEvent

Obviously, before settling on the word gremlin, I did a search to see if the word gremlin was already in use because, as we all know, most applications already have gremlins in their code.

To begin this task, I first piped results of the find command into the sed utility, for example:

find app -name "*.rb" -print | xargs sed -i '' 's/Event/Gremlin/g'
find app -name "*.rb" -print | xargs sed -i '' 's/event/gremlin/g'

I did this individually for files with suffixes for Ruby, ERB, Prawn, and jBuilder (.rb, .erb, .prawn, .jBuilder) and I split out the renames of the RSpec files until after all the app directory code was processed.

This do-over approach worked very well for me. My unit tests found plenty of problems but it was easy to modify the script to correct those issues and do it over again.

I’d like to thank Unit Tests and I’d also like to thank Git. And I guess a shout-out to sed is in order as well. Because of these tools, coupled with the do-over approach, I was able to quickly do something that was potentially very error prone with confidence and gusto.

The Final Do-Over Script:

git clean -df
git checkout -- .

# uncomment migration rollbacks after you've run the rename migration the first time
# rake db:rollback
# rake db:rollback RAILS_ENV=test

# 1) Rename Event to Gremlin

mv app/controllers/events_controller.rb app/controllers/gremlins_controller.rb
mv app/models/event.rb app/models/gremlin.rb
mv app/models/scene_event.rb app/models/scene_gremlin.rb
mv app/views/events app/views/gremlins
mv app/views/gremlins/_scene_event_fields.html.erb app/views/gremlins/_scene_gremlin_fields.html.erb
## scan/replace Event to gremlin
find app -name "*.rb" -print | xargs sed -i '' 's/Event/Gremlin/g'
find app -name "*.erb" -print | xargs sed -i '' 's/Event/Gremlin/g'
find app -name "*.prawn" -print | xargs sed -i '' 's/Event/Gremlin/g'
find app -name "*.jbuilder" -print | xargs sed -i '' 's/Event/Gremlin/g'
## scan/replace event to gremlin
find app -name "*.rb" -print | xargs sed -i '' 's/event/gremlin/g'
find app -name "*.erb" -print | xargs sed -i '' 's/event/gremlin/g'
find app -name "*.prawn" -print | xargs sed -i '' 's/event/gremlin/g'
find app -name "*.jbuilder" -print | xargs sed -i '' 's/event/gremlin/g'

# 2) Rename Incident to Event

## rename incident files and directories to event
mv app/assets/javascripts/incidents.js.coffee   app/assets/javascripts/events.js.coffee
mv app/assets/stylesheets/areas/_incidents.scss app/assets/stylesheets/areas/_events.scss
mv app/controllers/apis/incidents_controller.rb app/controllers/apis/events_controller.rb
mv app/controllers/incidents_controller.rb      app/controllers/events_controller.rb
mv app/models/incident.rb                       app/models/event.rb
mv app/models/scene_incident.rb                 app/models/scene_event.rb
mv app/services/incident_search.rb              app/services/event_search.rb
mv app/views/incidents                          app/views/events
mv app/views/events/_scene_incident_fields.html.erb app/views/events/_scene_event_fields.html.erb
## scan/replace Incident to Event
find app -name "*.rb" -print | xargs sed -i '' 's/Incident/Event/g'
find app -name "*.erb" -print | xargs sed -i '' 's/Incident/Event/g'
find app -name "*.prawn" -print | xargs sed -i '' 's/Incident/Event/g'
find app -name "*.jbuilder" -print | xargs sed -i '' 's/Incident/Event/g'
# scan/replace incident to event
find app -name "*.rb" -print | xargs sed -i '' 's/incident/event/g'
find app -name "*.erb" -print | xargs sed -i '' 's/incident/event/g'
find app -name "*.prawn" -print | xargs sed -i '' 's/incident/event/g'
find app -name "*.jbuilder" -print | xargs sed -i '' 's/incident/event/g'

# 3) Rename Gremlin to ScheduledEvent
## rename gremlin directories to scheduled_event
mv app/controllers/gremlins_controller.rb app/controllers/scheduled_events_controller.rb
mv app/models/gremlin.rb app/models/scheduled_event.rb
mv app/models/scene_gremlin.rb app/models/scene_scheduled_event.rb
mv app/views/gremlins app/views/scheduled_events
mv app/views/scheduled_events/_scene_gremlin_fields.html.erb app/views/scheduled_events/_scene_scheduled_event_fields.html.erb
## scan/replace gremlin to ScheduledEvent
find app -name "*.rb" -print | xargs sed -i '' 's/Gremlin/ScheduledEvent/g'
find app -name "*.erb" -print | xargs sed -i '' 's/Gremlin/ScheduledEvent/g'
find app -name "*.prawn" -print | xargs sed -i '' 's/Gremlin/ScheduledEvent/g'
find app -name "*.jbuilder" -print | xargs sed -i '' 's/Gremlin/ScheduledEvent/g'
## scan/replace gremlin to scheduled_event
find app -name "*.rb" -print | xargs sed -i '' 's/gremlin/scheduled_event/g'
find app -name "*.erb" -print | xargs sed -i '' 's/gremlin/scheduled_event/g'
find app -name "*.prawn" -print | xargs sed -i '' 's/gremlin/scheduled_event/g'
find app -name "*.jbuilder" -print | xargs sed -i '' 's/gremlin/scheduled_event/g'

# rspec renaming

mv spec/controllers/events_controller_spec.rb spec/controllers/scheduled_events_controller_spec.rb
mv spec/factories/events.rb spec/factories/scheduled_events.rb
mv spec/controllers/apis/incidents_controller_spec.rb spec/controllers/apis/events_controller_spec.rb
mv spec/factories/incidents.rb spec/factories/events.rb
mv spec/files/incidents1.csv spec/files/events1.csv
mv spec/files/incidents2.csv spec/files/events2.csv
mv spec/files/incidents3.csv spec/files/events3.csv
mv spec/files/incidents4.csv spec/files/events4.csv
mv spec/files/incidents_bad_date.csv spec/files/events_bad_date.csv
mv spec/files/incidents_gsub_issue.csv spec/files/events_gsub_issue.csv
mv spec/files/incidents_illegal_quotes.csv spec/files/events_illegal_quotes.csv
mv spec/files/incidents_no_date.csv spec/files/events_no_date.csv
mv spec/files/incidents_no_source.csv spec/files/events_no_source.csv
mv spec/files/incidents_osc_summary.csv spec/files/events_osc_summary.csv
mv spec/files/incidents_osc_summary_8.csv spec/files/events_osc_summary_8.csv
mv spec/files/incidents_osc_summary_osc_id_simple.csv spec/files/events_osc_summary_osc_id_simple.csv
mv spec/files/incidents_source_issue.csv spec/files/events_source_issue.csv
mv spec/files/incidents_with_no_errors.csv spec/files/events_with_no_errors.csv
mv spec/files/incidents_with_odd_quotes.csv spec/files/events_with_odd_quotes.csv
mv spec/models/incident_spec.rb spec/models/event_spec.rb

find spec -name "*.rb" -print | xargs sed -i '' 's/Event/ScheduledEvent/g'
find spec -name "*.rb" -print | xargs sed -i '' 's/event/scheduled_event/g'
find spec -name "*.rb" -print | xargs sed -i '' 's/Incident/Event/g'
find spec -name "*.rb" -print | xargs sed -i '' 's/incident/event/g'
find spec -name "*.rb" -print | xargs sed -i '' 's/INCIDENT/EVENT/g'

## correct sed script renames that had incorrectly changed a class with a similar suffix:
find spec -name "*.rb" -print | xargs sed -i '' 's/Noteworthy scheduled_event/Noteworthy event/g'
find spec -name "*.rb" -print | xargs sed -i '' 's/noteworthy_scheduled_event/noteworthy__event/g'
find app -name "*.rb" -print | xargs sed -i '' 's/noteworthy_scheduled_event/noteworthy_event/g'
find app -name "*.erb" -print | xargs sed -i '' 's/noteworthy_scheduled_event/noteworthy_event/g'

# Re-Import changes to routes and the migration from a temporary directory
cp ~/Downloads/routes.rb config
cp ~/Downloads/20160628002534_rename_incident_events.rb db/migrate

rake db:migrate
rake db:migrate RAILS_ENV=test

annotate

rspec spec