5-bullet-sunday-morning

I’m currently reading Principles by Ray Dalio

When your name is Ray Dalio, you dont need a thought-provoking title nor a fancy cover to have a bestseller. It is a thick book and with a hardbound black cover, it can pass as the Bible. Just like the Bible, this is not a book you read in one sitting nor something to pass the time. I had to pause, think about what he said, question it, and see if I can apply it to my life.

Here is Ray Dalio’s popular TED talk.

Bitcoin is so hot I need to mention it

A friend messaged me complaining how complicated and tedious it is to trade Bitcoin and other cryptocurrencies. Worse, he is just halfway to all the things he need to do. We haven’t fully discussed what a wallet is for, why a piece of paper can be so important in the Internet age, and why he needs a water-tight fire-proof vault :) He made the leap even though he still doesn’t fully understand what all these currencies are and I can only imagine how these steps can feel like just a waste of time.

He is not alone and this is good thing.

Yes, I am channeling my inner Stoicism here so here me out. Massive financial gains happen when you got in an opportunity before the general public does. The Bitcoin ecosystem right now is not much different from the early days of the Internet — with all the possibilities, confusion, as well as danger of another bubble.

If you feel the pain, look at it as your ticket to an exciting game. Let us enjoy the game and I wish you good luck.

On watching Cirque du Soleil

I first heard of Cirque du Soleil about 4 years ago. Whenever they are in Vancouver, they would have a big tent setup in the parking lot near a train station. Around this time, I would see that tent on way to and from my work. It is like one mysterious event. You don’t see much from the outside but people are lining up and you hear conversations in the train on how breathtaking the show is.

When I heard they are coming again, we made sure we will not let it pass this time, damn the cost. I am glad we did not. My kids love it. I love it. Seeing people fly and do stunts in a 3D theatre is fun though we know it is all computer magic and stunt doubles. But watching people somersault in the air with only a rope on one arm is way different fun. You can actually feel the danger with it.

I am grateful I get to watch this with my family.

This thing in JavaScript

Finished some modules from Wes Bos JavaScript course, so why not apply it right away. So I did and I thought this was no a brainer.

$('#filters').on('change', () => {
  console.log($(this).val());
});

Except that it is not and I paid some scarce hair for it. The right solution is to use regular function.

$('#filters').on('change', function () {
  console.log($(this).val());
});

Ah the binding. When using arrow function, this is not bound to the on scope. Instead, it just inherited whatever the parent scope is. Lesson learned.

Now I get it. So I thought.

$('#filters').on('change', function () {
  console.log($(this).val());
  setTimeout(function () {
    console.log($(this).val());
  });
});

Damnit, same error again. Because inside the function passed to setTimeout, this is bound to setTimeout. Hence, the same error. To fix it, just use arrow function.

$('#filters').on('change', function () {
  console.log($(this).val());
  setTimeout(() => {
    console.log($(this).val());
  });
});

Confusing, eh? Just remember to go back to the binding rules for the arrow function and now it makes sense because we want to have this refer to the parent scope.

Quote for you

“A side effect of doing challenging work is that you’re pulled by excitement and pushed by confusion at the same time.” - James Clear

5-bullet Sunday Morning

On Javascript, 1–1s, scholarships, bitcoin, and evil things.

As a developer:

Modern JavaScript Explained For Dinosaurs

Even though I’m already familiar with the tools mentioned in the article, this provided me a better understanding of the front-end ecosystem. It also helped that the headline resonated with me :) The article started with doing it old school — just like many of us dinosaurs did decades ago and moved on to modern techniques. It’s like resetting your brain and learning things again but only this time you come out with a better understanding.

On related note, I’m currently taking the ES6 course by Wes Bos. Just finished the Promises module and I say it’s totally worth it.

As a manager:

I’m thinking about 1–1s, why we do it and how to make it worthwhile for both parties. 1–1s that turn into status updates that can easily (and more efficiently) be done via email is a waste of everyone ’s time. But if you view 1–1s as an opportunity to create a connection between you and your manager, it is a very effective tool to build great working relationships that will go beyond your current workplace.

Don’t think of it as a meeting but rather a coffee with someone you are eager to know better.

As a parent:

My wife and I, together with our Grade 11 son, attended a college scholarship seminar sponsored by my son’s school. The school’s theatre is packed but according to my son, it is just a fraction of the student population that would benefit from it. I wonder why there aren’t that much interest on such an opportunity. My hypothesis is many students (and even parents) think applying for college scholarships is done when you are close to graduation time.

My takeaway from the seminar is that going after the smaller grants (e.g. $500 — $5000) will give you a better chance of hitting your scholarship goals. Why? Because there are so many of them (at least in Canada) and the chance of getting accepted to at least 10 is not unrealistic. Add those amounts and now your college is free. On the other hand, if you aim for the lottery, not only you are competing against the best and the brightest students who found a cure to cancer or ended hunger in Africa, there is just a handful of slots available.

If you’re a teacher or member of the parent council, and if you happen to be in BC Canada, I recommend you book Brittany Palmer.

On Bitcoin and cryptocurrencies:

There is a lot of hype going around and we shouldn’t be shocked if 99% of the cryptocurrencies out there turn out to be scams. Many people don’t know, including those who now call themselves “investors”, that these cryptocurrencies have 2 sides — the money and the technology. What you will often hear is the money side. After all, a headline that says some “kid became a millionaire after investing $100” will always grab our attention.

Of course, as an “investor”, I am happy with the success of the cryptocurrencies but even if the prices go down to zero, the technology (hint: it is not just blockchain) will stay and evolve in the coming years. And that is where the real benefits lie.

Cryptocurrencies are here to stay. But don’t be stupid.

I’m pondering:

What if evil is part of life? That the person doing bad things is just doing his job? Like a normal job, some are bad at it, some are doing it really well, and some are simply looking to change job? On that note, Mindhunter looks to be another interesting series from Netflix. But that has to wait because it is Stranger Things Season 2 baby.

This post was also published at Medium.

Am I Too Old to Be a Senior Developer?

Or, aren’t you too young to be a Senior developer?

I never had the title of Senior Developer until about 3 years ago. By that time, I already have 15 years of experience in creating, deploying and fixing software. This is not counting the years I spent freelancing for various tech companies while I was in college. I have worked in startups and big companies, led teams, launched several products on my own and failed, and numerous times I had to learn stuff not related to coding just to get that freaking software out the door. Suddenly, I feel so old and it feels so jurassic now to remember my Visual Basic program running on Windows 95 - ah the good old days!

I imagine these are more than enough to earn a senior title. On the other hand, I’ve worked with developers with a senior title who produced amateurish work. It made me wonder when should companies give the “senior” title to someone. More importantly, when can a developer, call oneself a senior in this field?

In the early stages of a project, the team usually has a number of technical options. A senior developer can articulate the pros and cons of a technical decision - the why and when, not just the how and what. This could be as simple as avoiding early optimizations, highlighting roadblocks the team will face, or avoiding unnecessary work.

An effective signal to know someone is truly a senior developer (or in that mindset regardless of title) is when the developer looks at context when applying a theory. What problem are we solving? What is important and what is not? When to start and when to stop. How often have you seen over-engineered systems that are flexible in ways that don’t matter and inflexible in ones that do? This is simply because developers want to try the latest cool thing they’ve read.

There is also, looking at the big picture. Not every moment in software development is spent working on change-the-world tasks. It’s quite the contrary. Majority of your time is spent on boring tasks that must be completed to the same quality as any other potentially more interesting tasks. Have you met a developer who planned on resigning if he get to work on a legacy project, regardless if that project generates millions of dollars of revenue? I did. It made me think now, if he was more interested in showing off in his resume rather than adding value to the company.

If my thoughts above would be considered, does this mean a developer with 3 years of coding experience and brings tremendous value to the team, can now be called a senior developer? How about someone with 10 years experience but in reality is just experienced the same year 10 times?

Realization. It’s never about time - it’s the maturity.

On the other hand, how realistically can you gain maturity in a short amount of time? Can you compress 10 years worth of experience into 1?

Success in software development goes beyond technical skills. If you want to learn how to manage a crisis, you have to experience a crisis. If you want to have a high performing team, you have to learn to work together, which takes time. Hey, even when superstar athletes play together they rarely get success in their first two years.

Can we now agree that years of experience is misleading? Worthwhile experiences simply do not happen every day. You can try compressing it similar to what schools do, or read books that’s ideally based on the author’s years of experience. Still, while these are helpful, they are not good enough to deal with messy problems. In actual work, there are lots of compromises and very few assumptions. And you have to rely on your previous success and failures when dealing with new problems.

#til Elixir's Anonymous Function

fn is a keyword that creates an anonymous function.

sum = fn(a,b) ->
  a+b
end

IO.puts sum.(1,2)

Invoke using . (dot) similar to Ruby’s #call. Note the dot is not used for named function calls.

fb = fn
  {0, 0, c} -> "FizzBuzz"
  {0, b, c} -> "Fizz"
  {a, 0, c} -> "Buzz"
  {a, b, c} -> c
end

A single function with multiple implementations depending on the arguments passed. Elixir does not have assignments - yes you heard it right. Instead, it uses pattern matching then it binds the variable to the value. Note each implementation should have the same number of arguments.

IO.puts fb.({0,0,3})
# FizzBuzz
IO.puts fb.({0,2,3})
# Fizz
IO.puts fb.({1,0,3})
# Buzz
IO.puts fb.({1,2,3})
# 3

Since it is just a function, why not define it inside another function.

fizzbuzz = fn(n) ->
  fb = fn
    {0, 0, c} -> "FizzBuzz"
    {0, b, c} -> "Fizz"
    {a, 0, c} -> "Buzz"
    {a, b, c} -> c
  end

  fb.({rem(n,3), rem(n,5), n})
end

IO.puts fizzbuzz.(10)
IO.puts fizzbuzz.(11)
IO.puts fizzbuzz.(12)
IO.puts fizzbuzz.(13)
IO.puts fizzbuzz.(14)
IO.puts fizzbuzz.(15)
IO.puts fizzbuzz.(16)

We now have a baseline FizzBuzz solution with no conditional logic. Note rem(a,b) is an operator that returns the remainder after dividing a by b. The final solution uses the Enum module.

Enum.each(1..100, fn(n) -> IO.puts fizzbuzz.(n) end)

I’ve read the |> pipe operator before, so why not use it. The pipe operator simply takes the result of one expression and passes it as the 1st parameter to the next expression.

(1..100) |> Enum.each(fn(n) -> IO.puts fizzbuzz.(n) end)

Joy Ride in Ruby's Spaceship

Ruby’s <=> operator is commonly referred to as the spaceship in the community. You may not have played with it directly but I bet you have relied on it a lot of times. Because, every time you use sort an array, you are tapping in to the spaceship operator.

Now, why would you care? Because sometimes we need to sort things which does not have a natural ordering computers are accustomed to. Take for example sorting clothes that use S, M, and L to refer to their sizes. And to add more fun, how about putting XS, XL, and XXL into the mix.

Let’s start with a bare class and see what happens.

require 'minitest/autorun'

class Size
  attr_reader :size

  def initialize(size)
    @size = size.to_s.upcase
  end

  def to_s
    @size
  end
end

describe Size do
  let(:sizes) { %w[L S M].map { |s| Size.new(s) } }
  it { sizes.sort.map(&:to_s).must_equal %w[S M L] }

  let(:a) { Size.new('S') }
  let(:b) { Size.new('M') }

  it { (a > b).must_equal false }
  it { (a < b).must_equal true }
end

$> ruby size.rb                                                                                                     [2.1.2]
Run options: --seed 37770

# Running:

EEE

Finished in 0.001207s, 2486.1356 runs/s, 0.0000 assertions/s.

  1) Error:
Size#test_0001_anonymous:
ArgumentError: comparison of Size with Size failed
    size.rb:24:in `sort'
    size.rb:24:in `block (2 levels) in <main>'


  2) Error:
Size#test_0002_anonymous:
NoMethodError: undefined method `>' for #<Size:0x007f8a7bae6c50 @size="S">
    size.rb:29:in `block (2 levels) in <main>'


  3) Error:
Size#test_0003_anonymous:
NoMethodError: undefined method `<' for #<Size:0x007f8a7bae6390 @size="S">
    size.rb:30:in `block (2 levels) in <main>'

3 runs, 0 assertions, 0 failures, 3 errors, 0 skips

Our test failed and that’s good news, isn’t it? The first failure is because the default implementation of <=> doesn’t do all the comparison required to make sort work. To make this work, we need to implement a <=> that returns the following:

  • nil if the comparison does not makes sense
  • -1 if left side is less than right side
  • 1 if left side is greater than right side
  • 0 if left and right are the same

Let’s update our code to use the spaceship.

class Size
  attr_reader :size

  SIZES = %w[S M L].freeze

  def initialize(size)
    @size = size.to_s.upcase
  end

  def to_s
    @size
  end

  def <=>(other)
    position <=> other.position
  end

  protected

  def position
    SIZES.index(size)
  end
end

Some things to ponder in our implementation.

  • In Ruby, operator calls are just method calls where the left side is the receiver and right side is the argument. In other words, this is a <=> b is the same as a.<=>(b)
  • It is common practice to call the argument as other as you know the other object :)
  • We leverage an existing implementation of <=>. The method #index returns the position of an element in the array, which is a Fixnum. The Fixnum class already knows how to compare numbers.
  • We use protected to hide an implementation detail but at the same time allow us to use it within instance methods of objects of the same class.

Now, how about the other test failures? Do we need to implement the < and > operators as well? Fortunately, Ruby got our back. We just need to include the module Comparable and we’re good. But wait, there’s more! By including the Comparable module, we also get <=, >=, and == for free.

Here’s the full implementation with additional test scenarios, including a reverse sort.

require 'minitest/autorun'

class Size
  include Comparable

  SIZES = %w[S M L].freeze

  attr_reader :size

  def initialize(size)
    @size = size.to_s.upcase
  end

  def to_s
    @size
  end

  def <=>(other)
    position <=> other.position
  end

  protected

  def position
    SIZES.index(size)
  end
end

describe Size do
  let(:sizes) { %w[L S M].map { |s| Size.new(s) } }

  it { sizes.sort.map(&:to_s).must_equal %w[S M L] }
  it { sizes.sort { |a,b| b <=> a }.map(&:to_s).must_equal %w[L M S] }

  let(:a) { Size.new('S') }
  let(:b) { Size.new('M') }

  it { (a > b).must_equal false }
  it { (a < b).must_equal true }
  it { (a >= b).must_equal false }
  it { (a <= b).must_equal true }
  it { (a == b).must_equal false }
end

$> ruby size.rb                                                                                                     [2.1.2]
Run options: --seed 44807

# Running:

.......

Finished in 0.001490s, 4698.8003 runs/s, 4698.8003 assertions/s.

7 runs, 7 assertions, 0 failures, 0 errors, 0 skips

Exercise: Software versions often follow the convention major.minor.patch. Create a Version class that takes a string version, e.g. “10.10.3” and implement the <=> operator.

I’m currently reading Effective Ruby by Peter Jones and this post is based on Item 13: Implement Comparison via “<=>” and the Comparable Module.

You Can't Handle the Truth

Quick question. What’s the output of this code in Ruby?

amount = 0
if amount
  puts 'hey'
else
  puts ‘nah'
end

If you answered nah, you’re wrong. But it’s fine because this is one of the biggest gotchas for developers who are new to Ruby. Heck, even seasoned developers like myself sometimes forget this. I blame my college CS professors for putting too much C syntax in my brain.

Ruby has a simple rule for dealing with boolean values: everything is true except false and nil. This also means that every expression and object in Ruby can be evaluated against true or false. For example, you can have a method find that returns an object when it finds one or nil otherwise.

if  o = Customer.find_by(email: ‘stevej@rip.com’)
  puts o.name
else
  puts ‘not found it'
end

But it’s a different story when returning a numeric value because 0 evaluates to true.

false and nil can also be a common source of confusion because you have 2 values that can be false. Consider the default behaviour of Hash, which returns nil if the key does not exist. If you only factor in the nil scenario, you will have a problem when a key returns a false value - a common scenario with code that handles configuration or settings. In the case below, this will output missing key

h = {'a' => 1, 'b' => false}
key = ‘b'
if h[key]
  puts 'found a value'
else
  puts 'missing key'
end

If that’s enough confusion for you, consider this: true, false, and nil are just instances of a class.

irb> true.class
=> TrueClass
irb> false.class
=> FalseClass
irb> nil.class
=> NilClass

They are global variables but you can’t set any value to it which is fine. Otherwise, there will be chaos!

irb> true = 1
SyntaxError: (irb):18: Can't assign to true
true = 1

But, this is Ruby and we can always introduce chaos. Matz, the creator of Ruby, has given us this much power because he trusts that we know what we are doing.

irb> class Bad
irb>   def ==(other)
irb>     true
irb>   end
irb> end

irb> false == Bad.new
=> false
irb> Bad.new == false
=> true

What the heck just happened? Well, == is just another method call - the first is for the FalseClass instance while the second is for the Bad instance.

If you have been using Ruby for a while and wants to become better at it, I suggest you get a copy of Effective Ruby by Peter Jones.

Rails #permitted

I recently upgraded a personal app I use for learning new things in Rails. But when I upgraded from 4.1.4 to 4.1.12 I encountered this familiar error.

Customer.where(auth.slice(:provider, :uid)).first_or_initialize
ActiveModel::ForbiddenAttributesError: ActiveModel::ForbiddenAttributesError
from /Users/greg/.rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/activemodel-4.1.12/lib/active_model/forbidden_attributes_protection.rb:21:in `sanitize_for_mass_assignment`

Now I remember. It’s one of those mass assignments where you have to specify the ‘permitted’ values before you can continue. In Rails 4, it’s good practice to whitelist the attributes you receive in the controller and it goes something like this:

def user_params
  params.require(:user).permit(:username, :email, :password)
end

# somewhere in the controller
Customer.create(user_params)

Now, let’s use this idiom. It should be easy, right?

> auth.permit(:provider, :uid)
=> nil

Wait, that didn’t go as expected. How about just simply composing the hash?

> Customer.where(provider: auth[:provider], uid: auth[:uid]).first_or_initialize
=> #<Customer:0x007fd9168ffa88>

Interesting. #permit returns nil, using plain hash works, and #slice doesn’t.

> auth.slice(:provider, :uid).class
=> OmniAuth::AuthHash < Hashie::Mash

It shouldn’t matter what auth is as long as it behaves like what the Customer model expects. But what does the Customer model expects? Actually, the error message is telling us what it expects. In Rails, there is this module for mass assignment protection:

# https://github.com/rails/rails/blob/master/activemodel/lib/active_model/forbidden_attributes_protection.rb
module ActiveModel
  # Raised when forbidden attributes are used for mass assignment.
  #
  #   class Person < ActiveRecord::Base
  #   end
  #
  #   params = ActionController::Parameters.new(name: 'Bob')
  #   Person.new(params)
  #   # => ActiveModel::ForbiddenAttributesError
  #
  #   params.permit!
  #   Person.new(params)
  #   # => #<Person id: nil, name: "Bob">
  class ForbiddenAttributesError < StandardError
  end

  module ForbiddenAttributesProtection # :nodoc:
    protected
      def sanitize_for_mass_assignment(attributes)
        if attributes.respond_to?(:permitted?) && !attributes.permitted?
          raise ActiveModel::ForbiddenAttributesError
        else
          attributes
        end
      end
      alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
  end
end

Nothing fancy here. Rails does a simple check whether to allow mass assignment or not.

> auth.slice(:provider, :uid).permitted?
=> false

> { provider: auth[:provider], uid: auth[:uid] }.permitted?
NoMethodError: undefined method `permitted?' for {:provider=>"facebook", :uid=>"123"}:Hash

OmniAuth::AuthHash does not even allow it. Plain Hash works because it doesn’t even respond to #permitted.

Rebuild Rails Part 4

Now, it is time to build real pages in our super duper Tracks framework. We will support ERB and to do that we need the erubis gem.

# app/controllers/posts_controller.rb
class PostsController < Tracks::Controller
  def index
    locals = { title: "/posts/index" }
    render template: "posts/index", locals: locals
  end
end

# app/views/posts/index.html.erb
hello from tracks <%= title %>

# lib/tracks/controller.rb
def render(options)
  template_name = options.fetch(:template)
  locals = options.fetch(:locals)

  filename = File.join "app/views", "#{template_name}.html.erb"
  template = File.read(filename)
  erb = Erubis::Eruby.new(template)
  erb.result locals
end

$ ruby spec/application_spec.rb
Run options: --seed 12593

# Running:

..

Finished in 0.020498s, 97.5705 runs/s, 195.1410 assertions/s.

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

Our tests show we are good but it’s not even close to Rails' “magic”. First, let’s use the controller/action convention, i.e. if no template is passed to the render method, we should use the app/views/posts/index.html.erb template. We also modify our Controller to save the env data passed by Rack because we need to check the path.

# lib/tracks/controller.rb
module Tracks
  class Controller
    attr_reader :env, :controller_name, :action_name

    def initialize(env)
      @env = env
      extract_env_info
    end

    def extract_env_info
      _, @controller_name, @action_name, after = path_info.split("/")
    end

    def path_info
      env["PATH_INFO"]
    end

    def extract_template_name
      "#{controller_name}/#{action_name}"
    end
  end
end

We then update our render method to check for the template if it is not passed.

# lib/tracks/controller.rb
     def render(options)
-      template_name = options.fetch(:template)
+      template_name = options.fetch(:template) { extract_template_name }
       locals = options.fetch(:locals)

       filename = File.join "app/views", "#{template_name}.html.erb"
@@ -29,6 +43,5 @@ module Tracks
       erb.result locals
     end

Our next modification involves using the @instance_variables to pass values from the controller to the view files. To do that, we just need to pass the current binding to the eruby instance and it should pickup the instance variables we have in the controller.

In Rails, it is a bit more involved. There is a concept of view context. Rails collects the instance variables from the controller, then duplicates the values into the view context. The Ruby methods #instance_variables, #instance_variable_get, #instance_variable_set allow Rails to accomplish that.

def render(options={})
  template_name = options.fetch(:template) { extract_template_name }
  filename = File.join "app/views", "#{template_name}.html.erb"
  template = File.read(filename)
  erb = Erubis::Eruby.new(template)
  erb.result(binding)
end

We also update our render method and controller because we do not need the locals parameter.

# app/controllers/posts_controller.rb
class PostsController < Tracks::Controller
  def index
    @title = "/posts/index"
    render
  end
end

# app/views/posts/index.html.erb
hello from tracks <%= @title %>

We still have the extra render call in our controller. To remove it, we keep track of the call to render and if there’s no rendered result yet, we call render.

diff --git i/app/controllers/posts_controller.rb w/app/controllers/posts_controller.rb
index f7883e3..d7aa012 100644
--- i/app/controllers/posts_controller.rb
+++ w/app/controllers/posts_controller.rb
@@ -1,6 +1,5 @@
 class PostsController < Tracks::Controller
   def index
     @title = "/posts/index"
-    render
   end
 end
diff --git i/lib/tracks/controller.rb w/lib/tracks/controller.rb
index 0119c1b..3d5d75a 100644
--- i/lib/tracks/controller.rb
+++ w/lib/tracks/controller.rb
@@ -30,7 +30,12 @@ module Tracks

       controller_class_name = controller.capitalize + "Controller"
       controller_class = Object.const_get(controller_class_name)
-      controller_class.new(env).send(action)
+      controller_context = controller_class.new(env)
+      controller_context.send(action)
+
+      if controller_context.rendered_string.nil?
+        controller_context.render
+      end
+
+      controller_context.rendered_string
     end

     def render(options={})
@@ -38,7 +43,7 @@ module Tracks
       filename = File.join "app/views", "#{template_name}.html.erb"
       template = File.read(filename)
       erb = Erubis::Eruby.new(template)
-      erb.result(binding)
+      @rendered_string = erb.result(binding)
     end

We also update our controller and tests to cover the case of using render explicitly.

# app/controllers/posts_controller.rb
class PostsController < Tracks::Controller
  def index
    @title = "/posts/index"
  end

  def show
    @title = "/posts/index"
    render template: "posts/index"
  end
end

# spec/application_spec.rb
require_relative "spec_helper"

describe CrazyApp::Application do
  include Rack::Test::Methods

  def app
    CrazyApp::Application
  end

  it "should respond with /" do
    get "/"
    last_response.ok?.must_equal true
    last_response.body.strip.must_equal "hello from index.html"
  end

  it "should respond with different path" do
    get "/posts/index"
    last_response.ok?.must_equal true
    last_response.body.strip.must_equal "hello from tracks /posts/index"
  end

  it "should respond with different template" do
    get "/posts/show"
    last_response.ok?.must_equal true
    last_response.body.strip.must_equal "hello from tracks /posts/index"
  end
end

Rebuild Rails Part 3

Let’s continue our rebuild rails series by supporting controllers. We simplify things by assuming a controller/action path format and only support GET request.

We adjust our .call implementation to handle the new path format. We also introduce another method #render_controller_action that inspects the path_info and instantiates the right controller using Rails NameController convention.

# lib/tracks.rb
def self.call(env)
  path_info = env["PATH_INFO"]
  if  path_info == "/"
    text = Tracks::Controller.render_default_root
  else
    text = Tracks::Controller.render_controller_action(env)
  end

  [200, {"Content-Type" => "text/html"}, [text] ]
end

# lib/tracks/controller.rb
def self.render_controller_action(env)
  path_info = env["PATH_INFO"]
  _, controller, action, after = path_info.split("/")

  controller_class_name = controller.capitalize + "Controller"
  controller_class = Object.const_get(controller_class_name)
  controller_class.new.send(action)
end

$ ruby spec/application_spec.rb
Run options: --seed 55835
# Running:

.E

Finished in 0.015837s, 126.2865 runs/s, 126.2865 assertions/s.

  1) Error:
CrazyApp::Application#test_0002_should respond with different path:
NameError: uninitialized constant PostsController
    /code/crazy/lib/tracks/controller.rb:13:in `const_get'

When we run the test, you see it failed on PostsController which is what we expect since we haven’t implemented PostsController yet. Let’s add the controller now.

# app/controllers/posts_controller.rb
class PostsController < Tracks::Controller
  def index
    "hello from tracks /posts/index"
  end
end

# config/application.rb
require "./app/controllers/posts_controller"

$ ruby spec/application_spec.rb
Run options: --seed 2475

# Running:
..

Finished in 0.019152s, 104.4277 runs/s, 208.8555 assertions/s.

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

Automatic loading

Our tests pass but something’s not right. In Rails, there is no need to require every controller (or pretty much anything) to make it work. To support this feature in our framework, we need 2 things:

  • convert PostsController to posts_controller.rb; and
  • auto-require ‘posts_controller’

To tie these 2 together, we tap into Object.const_missing so our framework would know if a class has been used but not yet loaded.

We also update the $LOAD_PATH to include app/controllers folder so Ruby knows where to look.

# lib/tracks.rb
require File.expand_path("../tracks/helper", __FILE__)
require File.expand_path("../tracks/object", __FILE__)

# config/application.rb
require './lib/tracks'
$LOAD_PATH << File.expand_path("../../app/controllers", __FILE__)

module CrazyApp
  class Application < Tracks::Application
  end
end

# lib/tracks/helper.rb
module Tracks
  module Helper
    def self.to_underscore(string)
      string.scan(/[A-Z][a-z]+/).
      join('_').
      downcase
    end
  end
end

# lib/tracks/object.rb
class Object
  def self.const_missing(c)
    require Tracks::Helper.to_underscore(c.to_s)
    const_get(c)
  end
end

Our implementation of .to_underscore is limited compared to what’s supported in Rails.

irb(main):019:0> s = "PostsController"
=> "PostsController"
irb(main):020:0> s.scan(/[A-Z][a-z]+/).join('_').downcase
=> "posts_controller"

Also, since we call const_get inside const_missing, you will run into serious trouble if your file does not contain the expected class. Try changing the name of the class inside app/controllers/posts_controller.rb into something else and you will get this error.

$ ruby spec/application_spec.rb
Run options: --seed 60433

# Running:

.E
Finished in 0.043152s, 46.3478 runs/s, 46.3478 assertions/s.

  1) Error:
CrazyApp::Application#test_0002_should respond with different path:
SystemStackError: stack level too deep
    /Users/greg/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/forwardable.rb:174

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

Rebuild Rails Part 2

Before we continue with out rebuilding series, we should write some tests first :)

# spec/spec_helper.rb
ENV["RAILS_ENV"] ||= "test"

require "rack/test"
require "minitest/autorun"

require File.expand_path("../../config/application", __FILE__)


# spec/application_spec.rb
require_relative "spec_helper"

describe CrazyApp::Application do
  include Rack::Test::Methods

  def app
    CrazyApp::Application
  end

  it "should respond with /" do
    get "/"
    last_response.ok?.must_equal true
    last_response.body.strip.must_equal "hello from index.html"
  end

  it "should respond with different path" do
    get "/posts"
    last_response.ok?.must_equal true
    last_response.body.strip.must_equal "hello from tracks /posts"
  end

end

Just like your typical Rails test setup, we have a common spec_helper file. We use minitest/autorun which gives us rspec-style DSL out of the box. For our test, we need Rack::Test::Methods to use get and other http methods. We also need an app method that returns our Rack application to make the tests work.

$ ruby spec/application_spec.rb
Run options: --seed 57256

# Running:
..

Finished in 0.015864s, 126.0716 runs/s, 252.1432 assertions/s.

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

We’re all good. Awesome!