You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 |
+------------+--------------+-------------------+-------------------------------------------------------------------------------------------+
...
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.
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:
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:
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:
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:
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.
And then move everything to lib/setlist_parser/parser.rb:
classSetlistParser::ParserBOOKMARKS=/([#%\*\^\$\-&†]+|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):
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:
% 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.
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!
app/models/announcement.rb in Skyway to app/models/tour_bus/announcement.rb
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:
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:
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:
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!
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.