intermediate_ruby     about     articles

My walk-through of Noah Gibbs' Rebuilding Rails Course

Rebuilding Rails Course Notes #

Supposedly, the completion rate for online courses is rather low. I’ve certainly bought many more courses than I’ve finished.

I don’t want to do this with Noah Gibbs’ Rebuilding Rails course. He’s an expert by every measure of the word, and learning directly from him sounds like some of the best use of my time available.

This page will be my scratch-pad of notes, observations, gotchas, and more.

I’m working through this course a week or two after a friend started it, and I wanted to make sure I could document the things he recommended. For example, two days ago, he said something like:

When in doubt, add testing for at least Minitest, when the option presents itself. It’ll make working through one of the chapters or extensions easier.

I bought the course, read the introduction, and skimmed the first few chapters, and the last chapter. I’ve been working in Ruby and Rails for a while, so I didn’t need to do anything from a “setup” perspective.


Chapter 1: Zero to “It Works” #

The first command to run is bundle gem rulers.

I’ve not done lots with bundle, so I looked up bundle --help. Lots of output. Of import:

  bundle gem NAME [OPTIONS]      # Creates a skeleton for creating a rubygem

So bundle gem --help:

> bundle gem --help
Usage:
  bundle gem NAME [OPTIONS]

Options:
  --bin, -b, [--exe], [--no-exe]   # Generate a binary executable for your library.
          [--coc], [--no-coc]      # Generate a code of conduct file. Set a default with `bundle config set gem.coc true`.
  -e, [--edit=EDITOR]              # Open generated gemspec in the specified editor (defaults to $EDITOR or $BUNDLER_EDITOR)
          [--ext], [--no-ext]      # Generate the boilerplate for C extension code
          [--git], [--no-git]      # Initialize a git repo inside your library.
                                   # Default: true
          [--mit], [--no-mit]      # Generate an MIT license file. Set a default with `bundle config set gem.mit true`.
  -t, [--test=rspec]               # Generate a test directory for your library, either rspec or minitest. Set a default with `bundle config set gem.test rspec`.
          [--no-color]             # Disable colorization in output
  -r, [--retry=NUM]                # Specify the number of times you wish to attempt network commands
  -V, [--verbose], [--no-verbose]  # Enable verbose output mode

Creates a skeleton for creating a rubygem

OK. Off to the races.

> bundle gem rulers
Creating gem 'rulers'...
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):

I’ll initialize it with minitest. Worst case, I just won’t run those tests. This might be what my friend Mark was referencing earlier about “initialize the Gem with a test file!”

Cool. Initial commit done. I’m tracking this all in this repo.

Arg, running bundle gem NAME initializes the gem with its own git repo, so I’m going to update my directory structure.

Here’s what I have:

directory structure

And I’m going to bump the rulers directory up one level.

Nevermind. rm -rf‘d the directory. Rebuilding from scratch. (I was running into a problem renaming the branch from master to main, I should have git add/commit before running git branch -m master main)

Turns out I’ve now set the defaults from my first time through. Run:

ls ~/.bundle to see the files you’ve got related to bundler default.

cat ~/.bundle/config and you’ll see the defaults you just set:

defaults

Onward!

I’m now tracking this gem here: https://github.com/josh-works/rulers/tree/main

updated rulers.gemspec per the recommendation, ran:

$ gem build rulers.gemspec
$ gem install rulers-0.0.1.gem

Checking that it worked:

> gem list | grep rulers
rulers (0.0.1)

[Inspecting the output of gem list --help to find that -d flag]

> gem list -d rulers

*** LOCAL GEMS ***

rulers (0.0.1)
    Author: Josh Thompson
    Homepage: https://intermediateruby.com
    License: MIT
    Installed at: /Users/joshthompson/.rvm/gems/ruby-2.5.8

    Practicing Noah Gibb's Rebuilding Rails course

Looks like it worked! off to the races!

Commit 60065dc

Hello World, more or less #

cd-ing up a directory, making a new “app”, per the recommendation:

Done. All looks good:

making the app

Gotcha: Gem::InvalidSpecificationException #

Gem::InvalidSpecificationException

I ran into a minor problem when I modified the gem and tried to run gem build rulers.gemspec:

> gem build rulers.gemspec
WARNING:  See http://guides.rubygems.org/specification-reference/ for help
ERROR:  While executing gem ... (Gem::InvalidSpecificationException)
    rulers-0.0.1 contains itself (rulers-0.0.1.gem), check your files list

I tried deleting rulers-0.0.1.gem from my terminal, but the error still cropped up. I don’t remember the exact order that I went with, but it wasn’t until I git added the deleted file that the command worked.

I googled the error, found this StackOverflow suggestion.

So, I’m going to ignore the “compiled” gem going forward. Might end up .gitignore-ing it or something.

Followed the next steps, and now I’ve got “rackup” sourced/working from the rulers gem instead of the bestquotes app. Here’s links to the specific commits:

And here’s how we know in the browser:

hello from RoRulers

Chapter 1 Exercises #

Exercise 1: Add Debugging to the app #

diff --git a/lib/rulers.rb b/lib/rulers.rb
index 25760e3..ccf735f 100644
--- a/lib/rulers.rb
+++ b/lib/rulers.rb
@@ -3,6 +3,7 @@
 module Rulers
   class Application
     def call(env)
+      `echo debug > debug.txt`;
       [200, {'Content-Type' => 'text/html'},
       ["Hello from RoRulers"]]
     end

Then re-build/re-install the gem, restart rack, reload the page, and you’ll get a file titled debug.txt in your best_quotes directory.

$ cat debug.txt
debug

It’s not super exciting. I wanted more info.

For the record, I didn’t know that running shell commands inside of backticks would… run shell commands.

So that’s cool. But I wanted more info than this, so I tried logging env inside the debug statement, like so:

`echo debug env > debug.txt`;

And rebuild the gem, reinstall it, restart rack, reload the page… and I started getting parse errors and unexpected newlines. I knew I was getting closer.

So I tried some string interpolation:

`echo debug "#{env}" > debug.txt`;

Still not quite there.

Here’s what I was seeing:

no good

I know string interpolation and character escaping in Ruby isn’t trivial, but it’s also not that hard.

I googled ruby escape control characters string, clicked the first result, “Escaping Characters in Ruby” (AppSignal), skimmed it down to Escaping Interpolation, where they say:

Escaping interpolation

Ruby supports interpolation inside strings. But once again, not all string definitions are created equal. Interpolation only works in double quoted strings.

In the code example below we see that interpolation works in a double quoted string, but that Ruby escapes the interpolation sequence in a single quoted string, rendering it useless.

And they included the following snippet:

name = "world"

"Hello #{name}"
=> "Hello world"

'Hello #{name}'
=> "Hello \#{name}"

This is actually exactly what I wanted! I “upgraded” my double quotes around env to single quotes:

diff --git a/lib/rulers.rb b/lib/rulers.rb
index 25760e3..ccf735f 100644
--- a/lib/rulers.rb
+++ b/lib/rulers.rb
@@ -3,6 +3,7 @@
 module Rulers
   class Application
     def call(env)
+      `echo debug '#{env}' > debug.txt`;
       [200, {'Content-Type' => 'text/html'},
       ["Hello from RoRulers"]]
     end

And it worked!

One quick rebuild, reinstall, restart, reload later:

success

Here’s the commit

Exercise 2: Your Library’s Library #

The guide has us adding a new array method (it’s actually already an existing array method, but we’ll cross that bridge later) and it talks about the importance of git add-ing the new files, because when you call gem build rulers.gemspec, it checks with git itself to see what files to compile.

This is totally sensical, reasonable, etc.

In the gemspec, we’ve got:

spec.files         = Dir.chdir(File.expand_path('..', __FILE__)) do
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
end

I’ve used git ls-files quite a bit. Most recently, when figuring out how to get Rubocop to run only on modified files. I wrote about that general process extensively in intermediate_ruby_obstacle_course/rubocop. Take a gander there, if you’d like.

Anyway, all this to say - if you add a new file to your gem and don’t git add it, even if you gem build ... the files will be missing.

Also, I wanted evidence that this new method was working, so I named my new method summm:

Add Array#summm

In my best_quotes repo:

# /best_quotes/config/application.rb
require 'rulers'

module BestQuotes
  class Application < Rulers::Application
    puts "hi there"
    puts [1, 3, 5].sum # I added this to test the "base case"
    puts [1, 3, 5, 11].summm # and to test the new method from the gem
  end
end

Evidence that it succeeds:

Array#summm

Exercise 3: Test Early, Test Often #

I added rack-test and minitest to the rulers.gemspec file as a development_dependency.

I then noticed I already had a test directory with a test_helper inside of it, containing everything the tutorial wanted me to add. I think this is what I get by specifying “minitest” in the interactive gem new NAME command process.

The $LOAD_PATH is familiar to me, assuming it’s at all related to my Linux/Mac’s $PATH.

the __dir__ is probably related to __FILE__ and __LINE__, which I’ve encountered doing some commit tracing with the Pry gem and trying to implement the principles of deliberate practice with Avdi Grimm and the Rake gem.

OK, I’m not going to add more tests, even though I’m suggested that I do.

this gist by Josh Cheek is great! Tons of potential. I’ll upgrade my testing some day.

Current commit: https://github.com/josh-works/rulers/commit/d562747261bcf1f94af5b4e5de2bc73344576582

Exercise 4: Other Application Servers #

gem install unicorn
unicorn config.ru

Unicorn serves on localhost:8080 by default, but you can override to 3001 w/ unicorn -p 3001. This exercise was… easy.

Exercise 5: Ignoring Files #

Ah, he recommends doing what I’d suspected I should do earlier - .gitignore the compiled versions of the gem.

A one-line addition to the .gitignore:

https://github.com/josh-works/rulers/commit/cf9e1d295da17bdecf34ba45322bbc5780ba6287 And this concludes chapter 1.

Chapter 2: Your First Controller #

This went pretty smoothly, until I tried to run the changes to my best_quotes app.

I spent a little while on this error:

Uninitialized Constant

I poked around for a little bit, confirmed that I’d named things correctly, couldn’t figure it out.

I eventually had to pull up the Chapter 2 commit from Noah Gibb’s implementation of this project, and I found the problem.

In lib/rulers.rb, I forgot to require my new routing file.

Fixed that, and we were off to the races!

Also, I’m following along with Ben Orenstein’s “Improve your code quality” challenge. Today’s challenge was get rid of a warning you’re used to ignoring.

These warnings seemed like good candidates:

bad warnings

So I gem list‘ed the offending gems, picked ~a version at random~ the newest gem version, and [added it to my Gemspec]. Now when I gem build rulers.gemspec the output is nice and clean:

no warnings

9e730b9

UGH, stuff is still broken.

> gem build rulers-0.0.2.gem
Invalid gemspec in [rulers-0.0.2.gem]: undefined local variable or method `metadata' for Gem::Specification:Class
ERROR:  Error loading gemspec. Aborting.

Gotcha: Update the best_quotes Gemfile.lock to source the 0.0.2 version of rulers #

Ah, here’s the problem - I combed through the commit on the rulers gem for chapter 2, couldn’t see my problem. I looked at the chapter 2 commit for best_quotes, and found the issue:

diff --git a/Gemfile.lock b/Gemfile.lock
index d5880f6..81087d4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,7 +2,7 @@ GEM
   remote: https://rubygems.org/
   specs:
     rack (2.2.3)
-    rulers (0.0.1)
+    rulers (0.0.2)
       rack

you can manually increment the value, or run bundle update rulers.

Noah mentioned this at the beginning of the chapter. He said:

You may also need to “bundle update rulers” in best_quotes.

This didn’t land when I read it the first time, but I’m seeing it now.

I’m making progress…

Now I’m getting more errors. Le sigh. When I visit localhost:3001, I get:

NameError at /
uninitialized constant Controller

localhost:3001/hi:

NameError at /hi
uninitialized constant HiController

So, something is wrong, obviously. What?

Gotcha: Visit http://localhost:3001/quotes/a_quote, not http://localhost:3001/ #

Oh boy. I simply forgot to visit the intended URL. Wow.

I’d been running rackup -p 3001, and then visiting localhost:3001, expecting to see… well, not an error.

But for this to work, I needed to visit http://localhost:3001/quotes/a_quote, which Noah mentioned in his book.

So, nothing was broken. That was a lot of combing through code, trying to diff my code and his, and not understanding why it wasn’t working.

Well, now I know.

Noah even called this out explicitly. I wish I wasn’t so bad at reading:

If you didn’t quite get it, please make sure to include “quotes/ a_quote” in the URL, like you see above – just going to the root no longer works. If you see “Uninitialised constant Controller” then your URL is probably off.

Now I’m playing around with trying to get the tests to work, so I can stick a pry in various spots and see how this is all wired together.

Phew. Chapter 2, going slowly.

FWIW, as expected, if you stick a require 'pry'; binding.pry in the gem and “hit it from best_quotes, it doesn’t dump you into a Pry session, just throws an error.

There’s some missing links, like:

rack.rubyforge.org/doc/SPEC.html, so here’s the Wayback Machine version: https://web.archive.org/web/20170328021208/rack.rubyforge.org/doc/SPEC.html

CGI keeps getting used - it stands for Common Gateway Interface: https://en.wikipedia.org/wiki/Common_Gateway_Interface

Exercise 1: Debugging the Rack environment #

This is pretty self-explanatory. In the best_quotes app, string interpolate env into the browser.

Exercise 2: Debugging Exceptions #

This is also pretty self-explanatory.

Here’s what my diagnostics look like before adding the error handling:

no help with errors

I tried visiting, successively:

localhost:3001/quotes/a_quote 
localhost:3001/quotes/a_quotee 
localhost:3001/quotes/a_quoteee 
localhost:3001/quotes/no-quote-to-be-had 

Here’s our nice error output, once you add the def exception update:

better errors

Per Noah’s suggestion:

You should see a prettily-formatted page saying there was a RuntimeError at / quotes/exception. The page should also have a big stack trace.

In chapter 8 we’ll look deeply into Rack middleware and why you’re seeing that. That page isn’t built into your browser. You can turn it off by setting RACK_ENV to production in your environment. It’s a development-only Rack debugging tool that you’re benefiting from.

I wanted to run rackup in production mode. I guessed I could add a ENV=production on boot, but checked rackup --help first to confirm.

Re-running rackup with:

$ rackup -p 3001 --env production

And we get a generic (and familiar) error page:

nice error

Lastly, lets try the begin/end/rescue suggestion.

Yep. After rebuilding/re-installing the gem and restarting the server…

works as expected

Noah says:

Go into rulers/lib/rulers.rb and in your call method, add a begin/ rescue/end around the controller.send() call. Now you have to decide what to do if an exception is raised – you can start with a simple error page, or a custom 500 page, or whatever you like. Go to the page again in your browser and make sure you see the page you just added.

I don’t actually know how to add “a simple error page”. I suspect I’ll figure it out as I go forward, and I might come back to this particular exercise and try it out.

If I had to guess (and timebox myself to 5 min of experimenting) I would create error.html and then read the file, and pass the output of the file to the raise call.

My laptop is at 3% battery, lets see if I can do this.

Errno::ENOENT at /quotes/exception
No such file or directory @ rb_sysopen - ./error.html
Ruby 	/Users/joshthompson/.rvm/gems/ruby-2.5.8/gems/rulers-0.0.2/lib/rulers.rb: in read, line 19
Web 	GET localhost/quotes/exception

So close… The system is trying to read the file, cannot find it. 1%, I think this is the way to go, but I’ll have to finish it after finding power.

👋

… and we’re back. I biked from the table outside where I was working on the above, eventually made it back. Here’s the Strava map:

Here’s my Strava ride around OCNJ. It was a lovely day

Let’s get this error page working…

As I read through the end of the chapter, I saw:

Return the contents of a known file – maybe public/index.html?

So I’m going to move my error.html to public/error.html. Maybe it’ll be accessible then.

Oh, by the way, with the testing I’ve added below, I can now add a pry to my begin/rescue/end block, and update my test to as follows:

# test/application_test.rb
def test_error
  get "/test/invalid"
  
  assert last_response.ok?
end

And when I run it, I hit the pry.

Hm. I kicked another question into the software-technique.slack.com group, I’ll update this page when I get a response.

Gotcha: Address already in use - bind(2) for 127.0.0.1:3001 (Errno::EADDRINUSE) #

I almost didn’t mention this, but if you lose your terminal window while Rack is still running, and try to restart it, you’ll get the above error message.

use lsof -i :PORT_IN_QUESTION and it’ll show the details of the process using that port.

You can then use kill <PID> to kill the running process, and now you can restart rack

Adding a (temporarily functioning) test for the sake of exploration #

I didn't get a ton out of this chapter - I cannot figure out a good way to stick a `pry` in the key Gem methods and explore state by running tests.

In Noah’s Slack group, in the #rebuilding_rails channel, I asked for help stubbing out a tiny test, so I could stick a pry in various places, and play around with the responses.

Noah delivered! First, he opened with a warning - this app isn’t TDD-able, if I try to add tests, they’ll stop working fairly regularly as the library code changes:

But you are going to need to update your code as you change features in the framework. There’s not really a way around it.

To put it another way: there should not be an answer to the question, “yeah, but how do I build my tests so that they succeed no matter how I change the functionality of the framework?” :slightly_smiling_face:

That’s fine with me! I just, again, wanted to stick a pry in:

module Rulers
  class Application
    def get_controller_and_action(env)
      require "pry"; binding.pry  # explore!
      _, cont, action, after = env["PATH_INFO"].split('/', 4)
      cont = cont.capitalize
      cont += "Controller"
      
      [Object.const_get(cont), action]
    end
  end
end

I wanted to explore that Object.const_get call.

It sets me up to better understand:

module Rulers
  class Application
    def call(env)      
      if ENV['PATH_INFO'] == '/favicon.ico'
        return [404,
          {'Content-Type' => 'text/html'}, []]
      end
      
      require "pry"; binding.pry # explore!
      
      klass, act = get_controller_and_action(env)
      controller = klass.new(env)
      text = controller.send(act)
      [200, {'Content-Type' => 'text/html'},
        [text]]
    end
  end

So, here’s what he recommended. It isn’t that complicated, though I certainly wouldn’t have figured this out on my own:

diff --git a/test/application_test.rb b/test/application_test.rb
index 635e165..fe218c4 100644
--- a/test/application_test.rb
+++ b/test/application_test.rb
@@ -3,6 +3,11 @@
 # will be helpful eventually
 class TestApp < Rulers::Application
 end
+class TestController < Rulers::Controller
+  def myaction
+    "Hello"
+  end
+end

 class RulersAppTest < Minitest::Test
   include Rack::Test::Methods
@@ -12,7 +17,7 @@ def app
   end

   def test_request
-    get "/"
+    get "/test/myaction"

     assert last_response.ok?
     body = last_response.body

Great! No stick a pry in, perhaps at the top of the following methods:

  • Rulers::Application#call
  • Rulers::Application#get_controller_and_action

Now that I type these class/methods, I’m not sure why there’s in different files. They’re both Modules by the title Rulers, in the Application class. ¯\_(ツ)_/¯

Run the tests with:

$ ruby test/application_test.rb

And you’re off to the races!

Go ahead and explore all the code that we added. It makes so much more sense now, with me playing with it in a pry session, than it did with me just reading it. Maybe I’ll get over this “need” as I grow as a developer (🤞), but I still cannot just read the code and walk away feeling like I know what it’s doing.

I played around with the pry session for 10 minutes, exploring the order by which the code is being called, lots is making sense. Phew.

This all exists in:

b59c151

Exercise 3: Roots and Routes #

Chapter 3: Rails Automatic Loading #

💬 Comments

Post comment
Loading...