Playing With Fire

Exploring the web one Elixir at a time

Using Ewebmachine to create a link shortener (part 1)

In a slight change to the previous post, I will look in more detail at Ewebmachine. To help with this I am using the excellent tutorial on Webmachine that is in Seven Web Frameworks in Seven Weeks published by Pragmatic Programmers as a basis.

I can’t recommend the book highly enough. Not only does it cover webmachine but also another excellent framework for the Haskell language called Yesod.

Anyhow, back to the subject in question.

The tutorial concerns itself with creating a link shortening facility in some respects similar to tinyurl or bit.ly.

We’ll start by creating a new mix project. Where you do this is entirely up to you. I use a directory called Projects for this sort of stuff.

$ cd ~/Projects
$ mix new shortener
$ cd shortener

This will give you a new mix based project.

Next we’ll update the dependencies that are required - Ewebmachine. For a more indepth explantation on this read my previous post on this: Setting up Elixir and Ewebmachine.

$ vim mix.exs

Update the application function to:

def application do
    [applications: [:logger, :ewebmachine],
    mod: {ShortenerSupervisor, []}]
end

and update the private deps function to:

defp deps do
    [
      {:ewebmachine, "1.0.0", [github: "awetzel/ewebmachine"]}
    ]
end

Once done, write and quit the file and pull down the dependancies:

$ mix deps.get

This will go and do some git stuff and build the dependancies - ewebmachine, webmachine and mochiweb.

What we’ll do next is build the main work horse of the project, the core functionality to store and retrieve the urls from a data store. In the first pass at this we’ll use ETS. Later on we’ll discuss using DETS or even Mnesia to store this data, but for now, building the application out the temporary storage provided by ETS is sufficient.

The Shortener Url Storage Engine or ShortenUrlSrv is constructed as follows:

In the lib folder of your project, create a file shorten_url_srv.ex

$ vim lib/shorten_url_srv.ex

in this file enter:

defmodule ShortenUrlSrv do
  use GenServer

  @tab :shortened_urls
  @server __MODULE__

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, __MODULE__, name: @server)
  end

  def get_url(id) do
    GenServer.call(@server, {:get_url, id})
  end

  def put_url(url) do
    GenServer.call(@server, {:put_url, url})
  end

  def init(_) do
    :ets.new(@tab, [:named_table])
    {:ok, %St{next: 0} }
  end

  def terminate(_reason, _state) do
    :ok
  end

  def code_change(_oldvsn, ctx, _extra) do
    {:ok, ctx}
  end

  def handle_call({:get_url, id}, _from, state) do
    reply = case :ets.lookup(@tab, id) do
      [] -> {:error, :not_found}
      [{_id, url}] -> {:ok, url}
    end
    {:reply, reply, state}
  end

  def handle_call({:put_url, url}, _from, state) do
    %St{next: n} = state
    id = ShortenUrlSrv.b36_encode(n)
    :ets.insert(@tab, {id, url})
    {:reply, {:ok, id}, %St{next: n+1}}
  end

  def handle_call(_req, _from, state) do
    {:stop, :unknown_call, state}
  end

  def handle_cast(_req, state) do
    {:stop, :unknown_cast, state}
  end

  def handle_info(_info, state) do
    {:stop, :unknown_info, state}
  end

  def b36_encode(n) do
    Integer.to_string n, 36
  end
end

What you will notice if you look closely is that we use a struct in the code %St{}. This is a very simple one and is contained in file lib/St.ex

defmodule St do
  defstruct next: 0
end

The next thing to do, before we can try this out is to create a supervisor. So in lib/shortener_supervisor.ex enter:

defmodule ShortenerSupervisor do
  use Application

  def start(_type, _args), do: ShortenerSupervisor.Sup.start_link

  defmodule Sup do
    use Supervisor
    def start_link, do: :supervisor.start_link({:local, __MODULE__},__MODULE__, [])

    def init([]) do
      supervise([
        supervisor(ShortenUrlSrv, [])
      ], strategy: :one_for_one)
    end
  end
end

So, not withstanding any typos, you should be able to now go:

$ iex -S mix

and it should compile and give you the IEX REPL (and no exceptions). Given the IEX prompt, we can now test. First lets add in some URLs.

Interactive Elixir (1.0.0-rc2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> ShortenUrlSrv.put_url("http://www.test.com")

you should then get the response from the application:

0

This means that the URL has been saved and stored at index 0 in the ETS storage. To check this we can:

iex(2)> ShortenUrlSrv.get_url("0")

this should return:

{:ok, "http://www.test.com"}

The next thing that we need to do is add Ewebmachine resources, these allow us to generate the shortened URLs and to fetch them via HTTP. Add references to the resource files into the supervisor, so change the supervisor function in lib/shortener_supervisor.ex to

  supervise([
    supervisor(Ewebmachine.Sup,[[modules: [ShortenerShortenResource, ShortenerFetchResource],port: 18080]]),
    supervisor(ShortenUrlSrv, [])
  ], strategy: :one_for_one)

Next, lets build the ShortenerShortenResource file (lib/shortener_shorten_resource.ex):

defmodule ShortenerShortenResource do
  use Ewebmachine

  resource ['shorten'] do
    content_types_provided do: [{'text/plain', :to_text}]

    to_text do: ""

    allowed_methods do: [:POST]

    process_post do
      host = :wrq.get_req_header("host", _req)
      [{'url', url}] = :mochiweb_util.parse_qs(:wrq.req_body(_req))
      {:ok, code} = ShortenUrlSrv.put_url("#{url}")
      shortened = "http://#{host}/#{code}\n"
      {true, :wrq.set_resp_body(shortened, _req), _ctx}
    end
  end
end

and lastly the ShortenerFetchResource (lib/shortener_fetch_resource.ex):

defmodule ShortenerFetchResource do
  use Ewebmachine

  resource [:code] do
    previously_existed do
      code = :wrq.path_info(:code, _req)

      case ShortenUrlSrv.get_url("#{code}") do
        {:ok, url} -> {true, _req, '#{url}'}
        {:error, :not_found} -> {false, _req, _ctx}
      end
    end

    resource_exists do: false

    moved_permanently do: {{true, _ctx}, _req, _ctx}
  end
end

Once these are in place, we can test in the following way:

Terminal 1:

iex -S mix

Terminal 2:

$ curl -i -X POST http://localhost:18080/shorten --data 'url=http%3A%2F%2Fwww.test.com'
HTTP/1.1 200 OK
Server: MochiWeb/1.1 WebMachine/1.10.6 (no drinks)
Date: Sat, 20 Sep 2014 23:05:13 GMT
Content-Type: text/plain
Content-Length: 25

http://localhost:18080/0

$ curl -i http://localhost:18080/0
HTTP/1.1 301 Moved Permanently
Server: MochiWeb/1.1 WebMachine/1.10.6 (no drinks)
Location: http://www.test.com
Date: Sat, 20 Sep 2014 23:06:45 GMT
Content-Type: text/html
Content-Length: 0

And there we have it, basic api that we can use to shorten URLs and then retrieve and redirect. Next time, we’ll look at creating some HTML through templating so that we have a nice and friendly user interface to our services.

Previous Article