Skip to content

Instantly share code, notes, and snippets.

@qrush

qrush/install.md Secret

Last active May 9, 2016 14:17
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save qrush/8e9bad7bd77c1a5324186913f8c5c3e6 to your computer and use it in GitHub Desktop.
Save qrush/8e9bad7bd77c1a5324186913f8c5c3e6 to your computer and use it in GitHub Desktop.
Awesome Extractions Done Quick: Railsconf 2016

Install!

Let's get started extracting!! Note: Postgres is not required anymore!!!

Need the slide deck?

https://speakerdeck.com/qrush/awesome-extractions-done-quick

(This is helpful if you need to see the database schema quickly, too!)

If the internet is working...

Let's clone and get setup:

git clone git@github.com:qrush/skyway-railsconf2016.git skyway
cd skyway
./bin/setup -v

Didn't work?

git clone https://github.com/qrush/skyway-railsconf2016.git skyway

Using Ruby 2.3 (or 2.2.x?)

  • Remove the ruby line from the Gemfile
  • Change .ruby-version to the version of ruby you are using, or run:
ruby -v | awk '{print $2}' > .ruby-version

Level 0 not working?

git fetch origin
git checkout level-0
git merge origin/level-0

If not...

  • Find a flash drive
  • Copy the AEDQ folder to your local dev directory, unzip the zips inside, then in that folder:
cd skyway
./bin/setup -v

Level 0: Identify potential extractions

Basic extractions: Let's install tools to find low hanging fruit!

Just keep in mind: Each of these tools will generate a lot of feedback. We need to pare it down to find out what's useful to act on.

Reek: Code Smells

Install reek in your Gemfile (make sure to bundle after):

group :development do
  gem 'reek'
end

Run it with reek app. Let's fix a file!

% reek app/models/setlist.rb
app/models/setlist.rb -- 3 warnings:
  [16, 18]:DuplicateMethodCall: Setlist#to_s calls slot.to_s(options) 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [16, 18]:DuplicateMethodCall: Setlist#to_s calls slots.map 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]
  [16, 18]:DuplicateMethodCall: Setlist#to_s calls slots.map { |slot| slot.to_s(options) } 2 times [https://github.com/troessner/reek/blob/master/docs/Duplicate-Method-Call.md]

Refactor this to remove the warnings! My take on it:

diff --git a/app/models/setlist.rb b/app/models/setlist.rb
index a1ba3ba..e7b9df3 100644
--- a/app/models/setlist.rb
+++ b/app/models/setlist.rb
@@ -13,9 +13,22 @@ class Setlist < ActiveRecord::Base
 
   def to_s(options = {})
     if options[:without_notes]
-      "#{name}: #{slots.map { |slot| slot.to_s(options) }.join(' ')}"
+      setlist_without_notes
     else
-      [name, *slots.map { |slot| slot.to_s(options) }].join("\n")
+      setlist_with_notes
     end
   end
+
+  private
+    def setlist_without_notes
+      "#{name}: #{song_names(without_notes: true).join(' ')}"
+    end
+
+    def setlist_with_notes
+      [name, *song_names].join("\n")
+    end
+
+    def song_names(options = {})
+      slots.map { |slot| slot.to_s(options) }
+    end
 end

After that, running reek app/models/setlist.rb should return nothing (which means success!) What's yours look like?

  • Reek requires a lot of tuning - you and your team need to decide on what's important!
  • Beware of false positives! It's not all useful feedback.
  • This is a good first attack for what is low hanging fruit - ripe for extracting or refactoring!

Rubocop: Code Style Enforcement

Install rubocop (make sure to bundle after)

group :development do
  gem 'rubocop'
end

Run it with rubocop app Let's fix a file:

% rubocop app/models/article.rb

app/models/article.rb:2:3: C: Rails/Validation: Prefer the new style validations validates :column, presence: value over validates_presence_of.
  validates_presence_of :title, :body, :published_at
  ^^^^^^^^^^^^^^^^^^^^^
app/models/article.rb:8:18: C: Style/RegexpLiteral: Use %r around regular expression. (https://github.com/bbatsov/ruby-style-guide#percent-r)
    to_html.scan(/<p>(.*)<\/p>/).flatten.first
                 ^^^^^^^^^^^^^^
app/models/article.rb:16:29: C: Performance/StringReplacement: Use tr instead of gsub. (https://github.com/JuanitoFatas/fast-ruby#stringgsub-vs-stringtr-code)
    "#{id}-#{title.downcase.gsub(" ", "-")}"
                            ^^^^^^^^^^^^^^
app/models/article.rb:16:34: C: Style/StringLiteralsInInterpolation: Prefer single-quoted strings inside interpolations.
    "#{id}-#{title.downcase.gsub(" ", "-")}"
                                 ^^^
app/models/article.rb:16:39: C: Style/StringLiteralsInInterpolation: Prefer single-quoted strings inside interpolations.
    "#{id}-#{title.downcase.gsub(" ", "-")}"
                                      ^^^

Fix the file! Here's my diff:

diff --git a/app/models/article.rb b/app/models/article.rb
index f160b16..d6ca049 100644
--- a/app/models/article.rb
+++ b/app/models/article.rb
@@ -1,11 +1,11 @@
 class Article < ActiveRecord::Base
-  validates_presence_of :title, :body, :published_at
+  validates :title, :body, :published_at, presence: true
 
   scope :undrafted, -> { where(draft: false) }
   scope :published, -> { order(published_at: :desc) }
 
   def summary
-    to_html.scan(/<p>(.*)<\/p>/).flatten.first
+    to_html.scan(%r{<p>(.*)</p>}).flatten.first
   end
 
   def to_html
@@ -13,6 +13,6 @@ class Article < ActiveRecord::Base
   end
 
   def to_param
-    "#{id}-#{title.downcase.gsub(" ", "-")}"
+    "#{id}-#{title.downcase.tr(' ', '-')}"
   end
 end

After fixing the issues, rubocop will not return any warnings:

 % rubocop app/models/article.rb
Inspecting 1 file
.

1 file inspected, no offenses detected

Rubocop is great for setting a code standard and sticking to it. You don't need to adopt it for your whole app - just try a small section first!

Brakeman: Security Checkups

Install brakeman in your Gemfile (bundle after):

group :development do
  gem 'brakeman'
end

Run it with: brakeman. Let's fix a file.

...

Model Warnings:

+------------+--------------+-------------------+-------------------------------------------------------------------------------------------+
| Confidence | Model        | Warning Type      | Message                                                                                   |
+------------+--------------+-------------------+-------------------------------------------------------------------------------------------+
| High       | Announcement | Format Validation | Insufficient validation for 'video' using /youtube/. Use \A and \z as anchors near line 3 |
+------------+--------------+-------------------+-------------------------------------------------------------------------------------------+
...

Let's fix that one!

Before:

class Announcement < ActiveRecord::Base
  validates_presence_of :body
  validates_format_of :video, with: /youtube/, allow_blank: true

After:

class Announcement < ActiveRecord::Base
  validates_presence_of :body
  validates_format_of :video, with: URI::HTTPS::ABS_URI_REF, allow_blank: true

Done! No extra gems needed to validate a URL. If you haven't run brakeman in your Rails app yet - do it now! It's one of the best ways you can easily check to see if there's security issues with your app.

References

Level 1: Equip Tools

Let's start by getting a class out of app/ and into lib/.

Move the main ruby file first

mv app/models/parser.rb lib/parser.rb

Running rake will now show:

 15) Error:
BasicParserTest#test_transitions_are_marked:
NameError: uninitialized constant ActiveSupport::TestCase::Parser
    test/test_helper.rb:18:in `parse_show'
    test/models/parser_test.rb:5:in `block in <class:BasicParserTest>'

Weird! Why didn't it find the moved file? Well, Rails isn't automatically looking for it:

irb(main):002:0> Rails.configuration.autoload_paths
=> []

So, let's teach it! In config/application.rb, add:

  config.autoload_paths << Rails.root.join('lib')

Now back in rails console we'll see that lib was added:

irb(main):001:0> Rails.configuration.autoload_paths
=> [#<Pathname:/Users/qrush/Dev/skyway/lib>]

Run rake and we should be set!

Run options: --seed 28938

# Running:

.......................................

Finished in 0.864670s, 45.1039 runs/s, 80.9558 assertions/s.

39 runs, 70 assertions, 0 failures, 0 errors, 0 skips

Move the test next

Let's make a new directory for these tests and split them up:

mkdir test/parsers
touch test/parsers/basic_parser_test.rb
touch test/parsers/edge_parser_test.rb 

Don't forget to require 'test_helper' on the EdgeParserTest - also remove the old test:

rm test/models/parser_test.rb

Does it pass?

Run options: --seed 38161

# Running:

.......................................

Finished in 0.886158s, 44.0102 runs/s, 78.9926 assertions/s.

39 runs, 70 assertions, 0 failures, 0 errors, 0 skips

But wait! We didn't tell Rails about the new tests' directory. Why did that work? Let's dig into rails. In https://github.com/rails/rails/blob/v4.2.5.1/railties/lib/rails/test_unit/testing.rake#L18 -

  Rails::TestTask.new(:run) do |t|
    t.pattern = "test/**/*_test.rb"
  end

Nice! Don't let your tests be constrained by what Rails generates for you!

Ensuring everything still works

After completing each step/extraction - just run rake. There's an integration test for setlist editing that uses Capybara that should pass.

Level 2: Gem Mining

Extract further from lib to a real RubyGem!

  • Check out the level-2 branch to get started.
  • Stuck? Feel free to reference or run the level-2-finished branch.
  • Also check out the finished gem for reference, if you're having trouble or stuck.

Let's make a gem!

Hop up a directory and let's make a new gem! If you haven't tried this, Bundler has a generator built right in that saves an immense amount of time.

cd ..
bundle gem setlist_parser -t=minitest

It'll go through a little wizard about what will go in your gem. For now, the defaults will do:

Creating gem 'setlist_parser'...
Do you want to generate tests with your gem?
Type 'rspec' or 'minitest' to generate those test files now and in the future. rspec/minitest/(none): minitest
Do you want to license your code permissively under the MIT license?
This means that any other developer or company will be legally allowed to use your code for free as long as they admit you created it. You can read more about the MIT license at http://choosealicense.com/licenses/mit. y/(n): y
MIT License enabled in config
Do you want to include a code of conduct in gems you generate?
Codes of conduct can increase contributions to your project by contributors who prefer collaborative, safe spaces. You can read more about the code of conduct at contributor-covenant.org. Having a code of conduct means agreeing to the responsibility of enforcing it, so be sure that you are prepared to do that. Be sure that your email address is specified as a contact in the generated code of conduct so that people know who to contact in case of a violation. For suggestions about how to enforce codes of conduct, see http://bit.ly/coc-enforcement. y/(n): y
Code of conduct enabled in config
      create  setlist_parser/Gemfile
      create  setlist_parser/.gitignore
      create  setlist_parser/lib/setlist_parser.rb
      create  setlist_parser/lib/setlist_parser/version.rb
      create  setlist_parser/setlist_parser.gemspec
      create  setlist_parser/Rakefile
      create  setlist_parser/README.md
      create  setlist_parser/bin/console
      create  setlist_parser/bin/setup
      create  setlist_parser/.travis.yml
      create  setlist_parser/test/test_helper.rb
      create  setlist_parser/test/setlist_parser_test.rb
      create  setlist_parser/LICENSE.txt
      create  setlist_parser/CODE_OF_CONDUCT.md
Initializing git repo in /Users/qrush/Dev/setlist_parser

Great! Back in Skyway's Gemfile, let's add it:

gem "setlist_parser", path: "../setlist_parser"

Next up, in setlist_parser's setlist_parser.gemspec, remove the TODO lines. Rubygems considers gemspecs with TODO in certain properties as invalid, so we need to supply something else instead:

  spec.homepage      = "https://example.com/setlist_parser"
  spec.summary       = %q{Setlist parser}
  spec.description   = %q{Setlist parser}

If you're using a ruby version manager like rbenv or RVM, you'll probably need to let it know what version of Ruby we're using. For now, let's stick to 2.2.3 since that's what Skyway uses:

echo "2.2.3" > .ruby-version
bundle

Finally, rake test should fail:

% rake test
Run options: --seed 5279

# Running:

.F

Finished in 0.001275s, 1569.0102 runs/s, 1569.0102 assertions/s.

  1) Failure:
SetlistParserTest#test_it_does_something_useful [/Users/qrush/Dev/setlist_parser/test/setlist_parser_test.rb:9]:
Failed assertion, no message given.

2 runs, 2 assertions, 1 failures, 0 errors, 0 skips

Test it does something useful

Next up, we'll actually port over the code. Let's get two tests setup:

rm test/setlist_parser_test.rb
touch test/basic_parser_test.rb
touch test/edge_parser_test.rb

Then, copy the content over from the Rails app to the gem. I'll leave this as an exercise for the reader.

Back to the setlist_parser.gemspec, we'll need to add in dependencies on a few libraries:

  spec.add_runtime_dependency "activesupport", "~> 4.2"
  spec.add_runtime_dependency "activerecord", "~> 4.2"
  spec.add_development_dependency "sqlite3"

Our tests assumed that Rails was set up. Depending on your development philosophy - you may or may not be in favor of this. For now, we're going to assume that you are. Sorry if not! We're going to create a mini-Rails environment in our test suite that assumes a similiar database structure to Skyway's. Eventually, one could refactor it so it's not dependent at all on a database, and it passes just data structures around. That's a noble goal - but let's stick to something practical for now!

Let's add some ActiveRecord setup to test/test_helper.rb:

require 'active_support'
require 'active_record'

ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
# ActiveRecord::Base.logger = Logger.new(STDOUT)

# require '../skyway/db/schema.rb'
#
# OR

ActiveRecord::Schema.define do
  create_table :venue, force: true do |t|
  end

  create_table :shows, force: true do |t|
    t.integer :venue_id
    t.datetime :performed_at
    t.text :raw_setlist
    t.string :notes
  end

  create_table :setlists, force: true do |t|
    t.integer :show_id
    t.integer :position
    t.string :name
  end

  create_table :slots, force: true do |t|
    t.integer :setlist_id
    t.integer :song_id
    t.integer :position
    t.boolean :transition
    t.string :notes
  end

  create_table :songs, force: true do |t|
    t.string :name
    t.boolean :cover
  end
end

class Venue < ActiveRecord::Base
end

class Show < ActiveRecord::Base
  belongs_to :venue
  has_many :setlists
  serialize :notes, Array
  attr_accessor :raw_setlist
end

class Setlist < ActiveRecord::Base
  belongs_to :show
  has_many :slots
  has_many :songs, through: :slots
end

class Slot < ActiveRecord::Base
  belongs_to :setlist
  belongs_to :song
  serialize :notes, Array
end

class Song < ActiveRecord::Base
end

Next, we'll need to bring over the "raw setlists" from Skyway's test/fixtures directory. These are just simple text files that are used to test how parsing works. Let's move them over:

mkdir -p test/fixtures/raw_setlists
cp ../skyway/test/fixtures/raw_setlists/* test/fixtures/raw_setlists 

Back in the Rails app

Remove the local parser! We won't need it anymore:

rm -rf test/parsers lib/parser.rb

in app/models/show.rb, Replace Parser with SetlistParser:

  def self.parse(params)
    SetlistParser.parse(params).tap(&:save)
  end

Remove the parse_show method from test/test_helper.rb - it won't be needed anymore either. That should be it!

Test that it works!

Just run rake. There's an integration test for setlist editing that uses Capybara that should pass.

References

Level 3: Gem Detour

Dig into some best practices - more good ways to handle extractions + code shifts.

  • Check out the level-3 branch to get started.

Follow some best practices!

Luckily, Bundler does most of this for us. If you haven't already, check out the gem's:

  • README
  • YourLibrary::VERSION (at lib/your_library/version.rb)
  • Code of Conduct
  • Testing Setup

Long, long ago - this had to be done manually for every gem! This is the power of generators. As the community moves we can suggest better defaults for everyone. Yay!

Module approach

Let's convert the SetlistParser class to a module! Usually in a gem you'll want to do this, since having a class as the top-level constant becomes a burden eventually.

In lib/setlist_parser.rb:

require "setlist_parser/version"
require "setlist_parser/parser"

module SetlistParser
  def self.parse(options)
    Parser.new(options).parse
  end
end

In lib/setlist_parser/version.rb:

module SetlistParser
  VERSION = "0.1.0"
end

And then move everything to lib/setlist_parser/parser.rb:

class SetlistParser::Parser
  BOOKMARKS = /([#%\*\^\$\-&†]+|Note:)/i
  ...
end

Of course after - run rake to see if anything broke. Also rake in the Rails app too!

Consider less (specific) dependencies!

It's a great idea to depend on less RubyGems if possible. For example - if you're doing HTTP requests...can you just use Ruby's standard library to make them? Is it possible here to require less libraries? Let's find out!

First up, in the gemspec, let's change activesupport to a development dependency (and then bundle):

  spec.add_development_dependency "activesupport", "~> 4.2"

And does it pass? Run rake to find out:

...............

Finished in 0.353788s, 42.3983 runs/s, 70.6638 assertions/s.

15 runs, 25 assertions, 0 failures, 0 errors, 0 skips

For your gems: make sure to see if you really need all of the dependencies marked as runtime or not. Runtime gems add to your production bundles, which makes your Rails app boot slower, your deploys longer, more code to check when there's security issues...the list goes on.

Actually, since activesupport is a dependency of activerecord - we can remove that line entirely. Try removing it and seeing if it's still passing. (It should!)

Appraise it

Let's make it work with more than one version of Rails. We're going to use this gem from thoughtbot to test across versions easily. Add to your gemspec:

  spec.add_development_dependency "appraisal", "~> 2.1"

Then, in a new file named Appraisals:

appraise "rails-4" do
  gem "rails", "4.2.6"
end

appraise "rails-5" do
  gem "rails", "5.0.0.beta3"
end

Then, you'll need to: appraisal install:

% appraisal install
WARN: Unresolved specs during Gem::Specification.reset:
      rake (>= 0)
WARN: Clearing out unresolved specs.
Please report a bug if this causes problems.
Note: Run `appraisal generate --travis` to generate Travis CI configuration.
>> bundle check --gemfile='/Users/qrush/Dev/setlist_parser/gemfiles/rails_4.gemfile' || bundle install --gemfile='/Users/qrush/Dev/setlist_parser/gemfiles/rails_4.gemfile'
Resolving dependencies...
The Gemfile's dependencies are satisfied
>> bundle check --gemfile='/Users/qrush/Dev/setlist_parser/gemfiles/rails_5.gemfile' || bundle install --gemfile='/Users/qrush/Dev/setlist_parser/gemfiles/rails_5.gemfile'
Bundler can't satisfy your Gemfile's dependencies.
Install missing gems with `bundle install`.
Fetching gem metadata from https://rubygems.org/...........
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies....
...

After that, run: appraisal rake test:

 appraisal rake test
WARN: Unresolved specs during Gem::Specification.reset:
      rake (>= 0)
WARN: Clearing out unresolved specs.
Please report a bug if this causes problems.
>> BUNDLE_GEMFILE=/Users/qrush/Dev/setlist_parser/gemfiles/rails_4.gemfile bundle exec rake test
-- create_table(:venue, {:force=>true})
   -> 0.0047s
-- create_table(:shows, {:force=>true})
   -> 0.0005s
-- create_table(:setlists, {:force=>true})
   -> 0.0006s
-- create_table(:slots, {:force=>true})
   -> 0.0004s
-- create_table(:songs, {:force=>true})
   -> 0.0003s
Run options: --seed 47319

# Running:

DEPRECATION WARNING: You did not specify a value for the configuration option `active_support.test_order`. In Rails 5, the default value of this option will change from `:sorted` to `:random`.
To disable this warning and keep the current behavior, you can add the following line to your `config/environments/test.rb`:

  Rails.application.configure do
    config.active_support.test_order = :sorted
  end

Alternatively, you can opt into the future behavior by setting this option to `:random`. (called from test_order at /Users/qrush/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-4.2.6/lib/active_support/test_case.rb:42)
...............

Finished in 0.357000s, 42.0168 runs/s, 70.0281 assertions/s.

15 runs, 25 assertions, 0 failures, 0 errors, 0 skips
>> BUNDLE_GEMFILE=/Users/qrush/Dev/setlist_parser/gemfiles/rails_5.gemfile bundle exec rake test
-- create_table(:venue, {:force=>true})
   -> 0.0191s
-- create_table(:shows, {:force=>true})
   -> 0.0006s
-- create_table(:setlists, {:force=>true})
   -> 0.0005s
-- create_table(:slots, {:force=>true})
   -> 0.0004s
-- create_table(:songs, {:force=>true})
   -> 0.0004s
Run options: --seed 50174

# Running:

...............

Finished in 0.326185s, 45.9862 runs/s, 76.6437 assertions/s.

15 runs, 25 assertions, 0 failures, 0 errors, 0 skips

Woot! If you have multiple versions of Rails running in your organization definitely check this out.

References

Level 4: Engines

From Mark Phelps, engines help to:

  1. Define Your Domain Boundaries
  2. Colocate Code
  3. Clean Out the Cruft

They're fantastic! Definitely read up on Mark's post if you haven't ever played with an engine.

Create a Rails engine

Let's get it created! First, in a new directory (preferably one up from your Skyway install):

rails plugin new tour_bus --mountable

Check out the layout here...does it seem familiar? It's a little Rails app!

% tree
app
├── assets
│   ├── images
│   │   └── tour_bus
│   ├── javascripts
│   │   └── tour_bus
│   │       └── application.js
│   └── stylesheets
│       └── tour_bus
│           └── application.css
├── controllers
│   └── tour_bus
│       └── application_controller.rb
├── helpers
│   └── tour_bus
│       └── application_helper.rb
├── mailers
├── models
└── views
    └── layouts
        └── tour_bus
            └── application.html.erb

It's also a gem! Which means we'll need to change the gemspec to make it work first. Open up tour_bus.gemspec and remove the TODO entries. Let's also make the rails dependency a little more flexible:

  s.homepage    = "https://example.com/tour_bus"
  s.summary     = "Summary of TourBus."
  s.description = "Description of TourBus."
  
  s.add_dependency "rails", ">= 4.2", "< 6"

Next, before we do anything else - let's get it installed into Skyway. In the Gemfile:

gem 'tour_bus', path: '../tour_bus'

Then, bundle. If you run rails console, this should work:

Loading development environment (Rails 4.2.5.1)
irb(main):001:0> TourBus
=> TourBus

Moving code over

Great! Next up, we're going to start moving the Announcement class over from the main Skyway app to TourBus:

 rails g model announcement
      invoke  active_record
      create    db/migrate/20160430013603_create_tour_bus_announcements.rb
      create    app/models/tour_bus/announcement.rb
      invoke    test_unit
      create      test/models/tour_bus/announcement_test.rb
      create      test/fixtures/tour_bus/announcements.yml

Then, we'll need to copy over the code!

  1. app/models/announcement.rb in Skyway to app/models/tour_bus/announcement.rb
  2. test/models/announcement_test.rb in Skyway to test/announcement_test.rb

Just remember - make sure to scope Announcement as TourBus::Announcement !

After that, we'll need to bring over the migration. Open up the migration you generated in db/schema.rb, and we'll copy over the table from skyway's db/schema.rb:

class CreateTourBusAnnouncements < ActiveRecord::Migration
  def change
    create_table :tour_bus_announcements do |t|
      t.text :body
      t.string :video
      t.timestamps null: false
    end
  end
end

We won't need to run this migration on Skyway itself - but it will be necessary for any other apps that need the engine. We'll actually need a different migration to rename the table for this level. In production - you could just use table_name.

Test that engine!

First up we'll need to get a database going, so run from your engine's directory:

% rake db:migrate
== 20160430013603 CreateTourBusAnnouncements: migrating =======================
-- create_table(:tour_bus_announcements)
   -> 0.0013s
== 20160430013603 CreateTourBusAnnouncements: migrated (0.0015s) ==============

Finally, let's run the suite!

# Running:

.

Finished in 0.016394s, 60.9973 runs/s, 60.9973 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Well, that's boring - let's bring over the code from Skyway. Once that's done (just make sure to keep the TourBus module!):

% rake
Run options: --seed 35163

# Running:

......

Finished in 0.019086s, 314.3590 runs/s, 314.3590 assertions/s.

6 runs, 6 assertions, 0 failures, 0 errors, 0 skips

Cleaning up

Next up - we'll remove the code from the Rails app side and depend on the engine instead:

rm app/models/announcement.rb
rm test/models/announcement_test.rb
rm test/fixtures/announcements.yml

Now, let's try in console...

irb(main):002:0> TourBus::Announcement.first
  TourBus::Announcement Load (24.5ms)  SELECT  "tour_bus_announcements".* FROM "tour_bus_announcements"  ORDER BY "tour_bus_announcements"."id" ASC LIMIT 1
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  relation "tour_bus_announcements" does not exist
LINE 1: SELECT  "tour_bus_announcements".* FROM "tour_bus_announceme...                                             ^

Oops! We'll need to tell TourBus that our table was named announcments originally. We could just make a new migration to rename the table. Sometimes though, that's infeasible in production - for now we'll just tell ActiveRecord it's a different table name. In config/initializers/tour_bus.rb:

Rails.application.config.to_prepare do
  TourBus::Announcement.table_name = "announcements"
end

Great! Next up, we have a few references to Announcement in our code. We've only moved the model over for now - so it should be an easy find + replace in app/controllers for changing that to TourBus::Announcement. Here's the diff I had:

diff --git a/app/controllers/announcements_controller.rb b/app/controllers/announcements_controller.rb
index 74c3022..e37aa68 100644
--- a/app/controllers/announcements_controller.rb
+++ b/app/controllers/announcements_controller.rb
@@ -2,11 +2,11 @@ class AnnouncementsController < ApplicationController
   before_filter :require_admin
 
   def new
-    @announcement = Announcement.new(body: Announcement.last.try(:body))
+    @announcement = TourBus::Announcement.new(body: TourBus::Announcement.last.try(:body))
   end
 
   def create
-    @announcement = Announcement.new(announcement_params)
+    @announcement = TourBus::Announcement.new(announcement_params)
 
     if @announcement.save
       redirect_to root_path
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index 3a6a1fa..e43b2c9 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -7,7 +7,7 @@ class HomeController < ApplicationController
 
   def show
     @upcoming_shows = Show.upcoming.limit(5)
-    @announcement = Announcement.last || Announcement.new
+    @announcement = TourBus::Announcement.last || TourBus::Announcement.new
     @week_count = Show.where(["performed_at >= ? and performed_at <= ?", Date.today, Date.today.end_of_week]).count
   end
 end

After that, run rake and you should be all set! Now we're running on an engine! Woot! We're just scratching the surface of what engines can be used for. Please check out the bonus round for more!

References

Level 5: Bonus Round!

Gotten this far? Great! I've got some ideas about how you could continue learning here.

Extract the engine in Level 4 further

Move AnnouncementsController to TourBus. This will require a few more changes than just moving the controller - you'll have to mount the engine's routes too, and move the controller and its views over.

Extract another gem

There's a few more domain models that could be extracted that aren't too "core" to Skyway's domain model. A big target could be the Import model, which accepts a CSV and creates multiple Shows.

Clean up the app

Want some more practice with reek, brakeman, or rubocop? Try to clear some other files of errors. Reek's explanations of each smell are great to read too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment