Use multiple controllers in a Sinatra application

Writing a Sinatra app in the Classic way is very easy. What you need is to create a file called app.rb:

# app.rb

require "sinatra"

get "/" do
  "hello, world"
end

And then, create a Gemfile in the same directory of app.rb and run bundle install --path vendor/bundle to install dependencies:

# Gemfile

source 'https://rubygems.org'

ruby "2.2.2"

gem "sinatra", "1.4.6"
gem 'thin', "1.7.0"

In your terminal, run ruby app.rb to start the server. If you run curl localhost:4567 in your terminal, you will get hello, world.

When your application grows larger, you need add more endpoints and business logic inside app.rb. However, it becomes very hard to manage when it comes to a certain size.

Today, I am going to share you a way to break down endpoints into different controllers. To do so, we will need to write the Sinatra app in the Modular way.

Rewrite app.rb in the Modular way:

# app.rb
require "sinatra/base"


class App < Sinatra::Base
  get "/" do
    "hello, world"
  end
end

Create a new file called config.ru in the same directory of app.rb:

# config.ru
require "./app"

run App

In you terminal, run rackup config.ru -p 4567 to start the server and if you run curl localhost:4567, you will get the same result, hello, world.

Now, I want all my endpoints related to users in a controller called UsersController. Let’s create a new file called users_controller.rb in the same directory of app.rb:

# users_controller.rb

class UsersController < Sinatra::Base
  get "/users/:id" do
    route = env["sinatra.route"]
    response = {:endpoint => route}.to_json

    content_type :json
    status 200
    body response
  end

  post "/users" do
    route = env["sinatra.route"]
    response = {:endpoint => route}.to_json

    content_type :json
    status 200
    body response
  end

  delete "/users/:id" do
    route = env["sinatra.route"]
    response = {:endpoint => route}.to_json

    content_type :json
    status 200
    body response
  end
end

And then update app.rb to include users_controller.rb:

# app.rb

require "sinatra/base"
require "json"

require "./users_controller"

class App < Sinatra::Base
  use UsersController
  get "/" do
    "hello, world"
  end
end

Start the app in the terminal: rackup config.ru -p 4567 and you can hit your users endpoints:

$ curl -X GET localhost:4567/users/uuid
{"endpoint":"GET /users/:id"}

$ curl -X DELETE localhost:4567/users/uuid
{"endpoint":"DELETE /users/:id"}

$ curl -X POST localhost:4567/users
{"endpoint":"POST /users"}

Of course, you can keep adding more controllers, for example, OrdersController.

Here is all the code in my github

Display tab number and use cmd + number to switch tabs in MacVim

It is great that we can use cmd + number to navigate tabs in a MacVim window just like iTerm and Chrome.

Put the following code into your ~/.vimrc and do the magic for you:

if has("gui_macvim")

  " Switch to specific tab numbers with Command-number
  noremap <D-1> :tabn 1<CR>
  noremap <D-2> :tabn 2<CR>
  noremap <D-3> :tabn 3<CR>
  noremap <D-4> :tabn 4<CR>
  noremap <D-5> :tabn 5<CR>
  noremap <D-6> :tabn 6<CR>
  noremap <D-7> :tabn 7<CR>
  noremap <D-8> :tabn 8<CR>
  noremap <D-9> :tabn 9<CR>

  " Command-0 goes to the last tab
  noremap <D-0> :tablast<CR>
endif

And of course, you can use cmd + shift + [ to the previous tab and cmd + shift + ] to the next tab.

By default, MacVim does not display the tab index in the tab title. It is great that MacVim can display the index so that you can easily use cmd

  • number to navigate. You can put the following line into your ~/.vimrc:
if has('gui_running')
  set guitablabel=⌘%N@%M%t
endif

But somehow, it does not work for me. I have to use a key to toggle the setting:

function! ShowTabNumber()
  if has('gui_running')
    set guitablabel=⌘%N@%M%t
  endif
endfunction
map <F2> :call ShowTabNumber()<CR>

Show current folder as iTerm title

I feel it is great that iTerm can show current directory name as its title. I did some research and found out this:

# put it to your ~/.bash_profile
export PROMPT_COMMAND='echo -ne "\033];${PWD##*/}\007"'

Detailed explanation:

Piece-by-Piece Explanation: the if condition makes sure we only screw with $PROMPT_COMMAND if we’re in an iTerm environment

iTerm happens to give each session a unique $ITERM_SESSION_ID we can use, $ITERM_PROFILE is an option too

the $PROMPT_COMMAND environment variable is executed every time a command is run see: ss64.com/bash/syntax-prompt.html

we want to update the iTerm tab title to reflect the current directory (not full path, which is too long)

echo -ne “\033;foo\007” sets the current tab title to “foo”

see: stackoverflow.com/questions/8823103/how-does-this-script-for-naming-iterm-tabs-work

the two flags, -n = no trailing newline & -e = interpret backslashed characters, e.g. \033 is ESC, \007 is BEL

see: ss64.com/bash/echo.html for echo documentation

we set the title to ${PWD##*/} which is just the current dir, not full path

see: stackoverflow.com/questions/1371261/get-current-directory-name-without-full-path-in-bash-script

BTW, I am using iTerm2.

Merge multiple csv files to a single csv file

Recently, I tried to merge multiple csv files (with the same header) into a single csv file without header.

For example, I have two csv files, hi.csv and hi1.csv:

$ cat hi.csv
uuid,email
"i-am-uuid","i-am-email"

$ cat hi1.csv
uuid,email
"i-am-uuid-in-hi1.csv","i-am-email-in-hi1.csv"

And I want to merge them to be:

"i-am-uuid","i-am-email"
"i-am-uuid-in-hi1.csv","i-am-email-in-hi1.csv"

You can achieve this by:

$ find . -name "hi*.csv" | xargs -n 1 tail -n +2
"i-am-uuid","i-am-email"
"i-am-uuid-in-hi1.csv","i-am-email-in-hi1.csv"

And then you can pip the result into a new file:

$ find . -name "hi*.csv" | xargs -n 1 tail -n +2 > new_file.csv
$ cat new_file.csv
"i-am-uuid","i-am-email"
"i-am-uuid-in-hi1.csv","i-am-email-in-hi1.csv"

For more information about xargs, please see here

What if I want the new file to have a header?

Well, after you have the new_file.csv, you can insert the header into the beginning of the new_file.csv:

$ echo -e "$(head -n 1 hi1.csv)\n$(cat new_file.csv)" > new_file.csv
$ cat new_file.csv
uuid,email
"i-am-uuid","i-am-email"
"i-am-uuid-in-hi1.csv","i-am-email-in-hi1.csv"

You can also combine these two steps into one single command:

$ echo -e "$(head -n 1 hi1.csv)\n$(find . -name 'hi*.csv' | xargs -n 1 tail -n +2 )" > new_file.csv
$ cat new_file.csv
uuid,email
"i-am-uuid","i-am-email"
"i-am-uuid-in-hi1.csv","i-am-email-in-hi1.csv"

I hope it is useful for you.

passport-facebook with query parameters in the callback

Recently, I was having trouble adding query parameters to the callback url when using passport facebook strategy. I am going to share the solution that I figured out.

var Passport = require('passport');
var FacebookStrategy = require('passport-facebook').Strategy;
const Router = require("express").Router();
var fbConfig = {
  display: "popup",
  clientID: "YourFbClientId",
  clientSecret: "YourFbClientSecret",
  callbackURL: "http://localhost:8686/auth/facebook/callback",
  profileFields: ['id', 'name', 'gender', 'displayName', 'photos', 'profileUrl', 'email']
}

Passport.use(new FacebookStrategy(fbConfig,
  function(accessToken, refreshToken, profile, callback) {
    return callback(null, accessToken);
  }
));

Router.get("/auth/facebook", function(req, res, next) {
  var callbackURL = fbConfig.callbackURL + "?queryParams=" + req.query.queryParams;
  Passport.authenticate("facebook", { scope : ["email"], callbackURL: callbackURL })(req, res, next);
});

Router.get("/auth/facebook/callback", function(req, res, next) {
  Passport.authenticate("facebook", {
    callbackURL: fbConfig.callbackURL + "?queryParams=" + req.query.queryParams,
    failureRedirect: "/login",
    session: false
  })(req, res, next) },
  function(req, res) {
  console.log(req.query.queryParams);
  //do whatever you want
});

Now, you can start to pass query params to localhost:8686/auth/facebook to initiate Facebook auth and the query params will be available in the callbackUrl of Facebook, localhost:8686/auth/facebook/callback