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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Size
  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

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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.

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

1
2
3
4
5
6
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.

1
2
3
4
5
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

1
2
3
4
5
6
7
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.

1
2
3
4
5
6
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!

1
2
3
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.

1
2
3
4
5
6
7
8
9
10
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.

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.

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

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

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!

Found this interesting ruby quiz from AlphaSights. Given an array of hashes, collapse into an array of hashes containing one entry per day. And you can only reference the :time key and not the rest.

log = [
  {time: 201201, x: 2},
  {time: 201201, y: 7},
  {time: 201201, z: 2},
  {time: 201202, a: 3},
  {time: 201202, b: 4},
  {time: 201202, c: 0}
]

# result should be
[
  {time: 201201, x: 2, y: 7, z: 2},
  {time: 201202, a: 3, b: 4, c: 0},
]

The first thing came to mind is to use Enumerable#group_by

grouped = log.group_by { |i| i[:time] }
collapsed = grouped.collect do |t, a|
  no_time_h = a.inject({}) do |others, h|
    others.merge h.reject { |k, v| k.to_sym == :time }
  end

  {time: t}.merge(no_time_h)
end

puts collapsed.inspect

However, after reading this a couple of times, I still find the solution hard to follow. For starter, group_by returns a hash where the values are an array of hashes which brings me back to the original problem even though it is already grouped by time. That I feel made the rest of the code more complicated.

# result of group_by
{201201=>[{:time=>201201, :x=>2}, {:time=>201201, :y=>7}, {:time=>201201, :z=>2}], 201202=>[{:time=>201202, :a=>3}, {:time=>201202, :b=>4}, {:time=>201202, :c=>0}]}

For my second version, I simply loop into the array and compose the hash using :time as the key. Afterwards, use the key-value pair to compose the resulting array. The code may be longer but it is more readable. Remember, Correct, Beautiful, Fast (in That Order).

hash_by_time = {}
log.each do |h|
  time = h[:time]
  others = h.reject { |k,v| k.to_sym == :time }

  if hash_by_time[time]
    hash_by_time[time].merge! others
  else
    hash_by_time[time] = others
  end
end

collapsed = hash_by_time.collect do |k, v|
  {time: k}.merge(v)
end

Though I’ve seen this video a gazillion times, I still find it fresh and inspiring.

I have no plans to build another Rails-clone. Let’s leave that work to other smarter people with more time. But wouldn’t it be fun if we can learn how Rails work under the hood and find out what makes it “magical”? In this post I will only cover what happens when you typed that url until you get an HTML page. We’ll simplify further by not using any database access. If you would like to go deeper and wider, there is a book devoted entirely to it that I highly recommend.

Let’s call our application CrazyApp and let’s build our first web app.

# config.ru
app =  proc do |env|
  [200, {'Content-Type' => 'text/html'}, ["hello from crazy app"] ]
end

run app
$ rackup config.ru -p 3000                                                                       [2.0.0-p247]
Thin web server (v1.6.2 codename Doc Brown)
Maximum connections set to 1024
Listening on 0.0.0.0:3000, CTRL+C to stop
127.0.0.1 - - [16/Nov/2014 20:42:02] "GET / HTTP/1.1" 200 - 0.0008

rebuild1

It’s all about the Rack

Rack is a gem that sits between your framework (e.g. Rails) and Ruby-based application servers like Thin, Puma, Unicorn, and WEBrick. When you type a url, it goes through several layers of software until it hits our application which in this case just returns a hellow from crazy app. Rack simplifies the interface for web servers that we only have to worry about a few things to handle an HTTP request.

  • HTTP status, e.g. 200
  • Response headers. There are lot of things you can set here but for now let’s stick to content-type.
  • Actual content. In our case, an HTML page.

Let’s look at a boilerplate config.ru that you get from Rails.

# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment',  __FILE__)
run Rails.application

One step in Rails' bootup process is to define Rails.application as class MyApp::Application < Rails::Application. Both Rails::Application and proc provides a call method that is why both config.ru works.

Now, let’s move our initial config.ru code to a different class that we can later extract into a gem for our framework that we shall call Tracks. From here on, we shall follow Rails conventions and build our gem from there.

# config.ru
require './config/application'
run CrazyApp::Application

# config/application.rb
require './lib/tracks'

module CrazyApp
  class Application < Tracks::Application
  end
end

# lib/tracks.rb
module Tracks
  class Application
    def self.call(env)
      [200, {'Content-Type' => 'text/html'}, ["hello from tracks"] ]
    end
  end
end

rebuild2

Exit from your rackup process and re-run it because we are not supporting auto-reloading. This time you will see a message from our Tracks - the super awesome Rails-like framework.

Render a default page

We now introduce a very simple root controller and use it to render a default index page. We also modify our route handling by inspecting the value in env object that Rack passed to our framework. The env packs a lot of information about a request and for our routing, we are interested in PATH_INFO which is the url after the domain minus the query parameters.

# lib/tracks/controlle.rb
module Tracks
  class Controller
    def self.render_default_root
      filename = File.join('public', 'index.html')
      File.read filename
    end
  end
end


# lib/tracks.rb
require File.expand_path('../tracks/controller', __FILE__)

module Tracks
  class Application
    def self.call(env)
      path_info = env['PATH_INFO']
      if  path_info == '/'
        text = Tracks::Controller.render_default_root
      else
        text = "hello from tracks #{path_info}"
      end

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

rebuild3 rebuild4

That’s it for now. Next time, we will create our own controllers, action, and dynamic pages using ERB.

Just saw a simple exercise in my Facebook feed and I thought I give it a shot. The problem is simple:

Write a function that returns the number of vowels in the string.

Here’s my ruby solution:

require 'minitest/autorun'

def vowel_count(s)
  vowels = %w[a e i o u]
  s.to_s.scan(/\w/).select { |i| i if vowels.include?(i.downcase) }.count
end

describe "#vowel_count" do
  it "should count upcase lowercase" do
    test = "I wanted to be an astronaut"
    vowel_count(test).must_equal 10
  end

  it "should be zero for empty string" do
    vowel_count("").must_equal 0
  end

  it "should be zero for nil" do
    vowel_count(nil).must_equal 0
  end
end

Sounds simple, right? But there are subtle things you should watch out for.

  • Upper and lower cases may seem trivial but programmers are often bitten by these when comparing strings.
  • An initial solution would be to access each character via [index] and increment a counter for vowels. Here is where familiarity with your language’s libraries becomes useful. While I didn’t get the right method initially, I know Ruby’s String library offers a way to extract regex matches. From then on, it’s just a matter of using Enumerable#select which is a common Ruby idiom for filtering elements.
  • Having tests even for a simple code is a good discipline to have. My initial test only covers the functional requirement. When I added the case of nil it quickly showed the flaw in my code, which brings me to my next point.
  • Produce sensible results as much as possible. While you can argue the requirement states a string and not a nil, it is good habit to defend your code in case the caller passed an invalid value. Hence, I converted the parameter to a string to ensure the rest of the code is working with a string object and it gives a sensible result even if the passed parameter is not a string.

Minimalist testing

If you are working with Rails' for a while, you probably been pampered with Rails seamless integration with testing frameworks you’ll be forgiven if you think these support are only available within Rails.

Ruby comes with minitest/autorun that supports a minimalist testing framework. Just require in your code and you are good to go with rspec-style testing right off the bat.

$ ruby vowelcount.rb
Run options: --seed 47907

# Running:

...

Finished in 0.001155s, 2597.4026 runs/s, 2597.4026 assertions/s.

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