Using Ewebmachine to create a link shortener (part 1)
Posted on September 20, 2014 by Clive in Elixir, eWebmachine
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. 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. Update the application function to: and update the private deps function to: Once done, write and quit the file and pull down the dependancies: 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 in this file enter: 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 The next thing to do, before we can try this out is to create a supervisor. So in lib/shortener_supervisor.ex enter: So, not withstanding any typos, you should be able to now go: 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. you should then get the response from the application: This means that the URL has been saved and stored at index 0 in the ETS storage.
To check this we can: this should return: 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 Next, lets build the ShortenerShortenResource file (lib/shortener_shorten_resource.ex): and lastly the ShortenerFetchResource (lib/shortener_fetch_resource.ex): Once these are in place, we can test in the following way: Terminal 1: Terminal 2: 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.$ cd ~/Projects
$ mix new shortener
$ cd shortener
$ vim mix.exs
def application do
[applications: [:logger, :ewebmachine],
mod: {ShortenerSupervisor, []}]
end
defp deps do
[
{:ewebmachine, "1.0.0", [github: "awetzel/ewebmachine"]}
]
end
$ mix deps.get
$ vim lib/shorten_url_srv.ex
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
defmodule St do
defstruct next: 0
end
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
$ iex -S mix
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")
0
iex(2)> ShortenUrlSrv.get_url("0")
{:ok, "http://www.test.com"}
supervise([
supervisor(Ewebmachine.Sup,[[modules: [ShortenerShortenResource, ShortenerFetchResource],port: 18080]]),
supervisor(ShortenUrlSrv, [])
], strategy: :one_for_one)
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
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
iex -S mix
$ 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