En esta entrada estaremos implementando un servicio web muy común,
URL Shortening. Para lograr nuestros objetivos estaremos utilizando Ruby, Sinatra y Redis.
¿Por qué Sinatra?
Es cierto que hay entornos más completos y funcionales que Sinatra, pero la pega está en que los entornos populares como Rails requieren una pila de dependencias enorme y están orientados para aplicaciones web un poco más complejas y extensas. Sinatra es la opción ideal debido a su simplicidad y su cantidad mínima de dependencias.
más información
¿Por qué Redis?
Si bien se podría usar un Hash interno, que de manera más eficiente lleve el registro de todo. El uso de Redis agrega unas cuantas ventajas muy atractivas, como la persistencia de datos, tiempo de vida para los datos y la capacidad de acceder externamente y realizar métricas sobre los datos.
más información
El Algoritmo
De toda la aplicación, esta es la parte mas fácil de implementar, pero sin embargo, es la que requiere un poco más de cuidado de nuestra parte. Una mala elección en el algoritmo interno podría resultar en procesamiento innecesario, perdidas de tiempo y por que no, problemas de mantenimiento. A continuación listo algunos acercamientos junto con sus pros y contras.
- Strings Aleatorios
- PROS
- Fácil de implementar.
- CONS
- Se debe comprobar mucho.
- Se podría necesitar generar varias cadenas aleatorias para obtener una disponible.
- Strings Secuenciales
- PROS
- Fácil de implementar.
- No requiere comprobaciones.
- CONS
- Cualquier usuario podría predecir direcciones válidas.
- Permutación De Carácteres
- PROS
- Fácil de implementar.
- No requiere comprobaciones.
- Las direcciones no se pueden predecir.
- CONS
-
Para implementar el tercer acercamiento, solo basta con usar el método Array#permutation para generar las secuencias únicas, y para mitigar el problema de la predictibilidad, se baraja el array de carácteres con el método Array#shuffle y listo, solo queda iterar por todas las permutaciones.
class Url
def initialize(n=1)
@n = n
@seq = mkseq(@n)
end
def next
begin
@seq.next.join
rescue StopIteration
@n += 1
fail 'No more resources available!' if @n > CHARS.length
@seq = mkseq(@n)
self.next()
end
end
private
def mkseq(n, random=true)
if random
CHARS.shuffle.permutation(n)
else
CHARS.permutation(n)
end
end
CHARS = [*(0..9), *('A'..'Z'), *('a'..'z')]
end
Es muy importante señalar que es una mala idea convertir el resultado del método Array#permutation en un array, pues podría resultar en una perdida de tiempo del orden de minutos o días. Bueno, implementado ya el corazón de la aplicación, solo nos queda manejar las peticiones con Sinatra y unir todo.
La mecánica es la siguiente: Cuando un usuario hace una petición a nuestro servidor a través de una URL corta, se toma la segunda parte de la dirección
— el recurso o path — y se hace una petición a Redis, si dicho recurso existe, el usuario es redirigido al enlace devuelto por Redis utilizando el método
redirect, si no existe, entonces en vez de redirigir al usuario, se presenta un aviso con información al respecto.
#!/usr/bin/env ruby
#encoding: UTF-8
require './url.rb'
require 'redis'
rd_child, wr_parent = IO.pipe()
rd_parent, wr_child = IO.pipe()
if fork()
rd_child.close
wr_child.close
resources = Url.new
loop do
IO.select([rd_parent])
rd_parent.read(1)
wr_parent.write(resources.next)
end
else
rd_parent.close
wr_parent.close
require 'sinatra'
redis = Redis.new
set :static, true
set :public_folder, File.join(File.dirname(__FILE__), 'static')
get '/' do
erb :index #template
end
get '/:resource' do
@url = redis.get(params[:resource])
if @url
redirect(@url)
else
halt 400, '<html><head><title>Error</title></head><body><h1>Not Found</h1></body></html>'
end
end
post '/create' do
begin
URI.parse params[:url]
rescue
halt 400, '<html><head><title>Error</title></head><body><h1>Bad Url</h1></body></html>'
end
wr_child.write('g')
IO.select([rd_child])
@resource = rd_child.sysread(512)
redis.set(@resource, params[:url])
erb :index # tamplate
end
end
Hay un pequeño inconveniente del que no se habla mucho en la documentación oficial de Ruby. Los
Fibers no se pueden utilizar a través de múltiples hilos, por ejemplo si intentas ejecutar el siguiente código, producirá un error:
fiber = nil
Thread.new do
fiber = Fiber.new do { Fiber.yield 1 }
end
fiber.resume # FiberError: fiber called across threads
Debido a que el método Array#permutation utiliza Fibers y algunos servidores web como
Thin usan
Threads, en efecto, para evitar errores en tiempo de ejecución está la decisión de crear dos procesos en el código de arriba, uno para ejecutar Sintra y otro exclusivamente para generar combinaciones. Bien, ahora solo queda la parte web; el
front end.
Página Web
Bueno, como realmente el diseño es para los diseñadores, me haré de una página que genere una interfaz más o menos aceptable para luego descargar el código y modificarlo a gusto.
Para lograr una interfaz parecida a la de esta entrada, visita
http://www.phpform.org/ y selecciona a gusto los elementos de la página principal, descarga el código y personifícalo, recuerda, la página principal debe quedar lo más simple posible.
Una cosa más, para mostrar la nueva url al usuario, se utiliza el valor almacenado por la aplicación en la variable de instancia
@resource. Cada vez que se acorta un enlace, dicha variable es accesible desde el
view, en mi caso el fichero index.erb
— el cual elaboré
a partir del código generado por la pá
gina phpform —. El siguiente es un fragmento del código necesario para mostrar la URL recién creada en el pie (footer) de la pá
gina desde el view index.erb.
<div id="footer">
<% if @resource %>
<% url = "http://localhost:4567/#@resource" %>
<p><a href="<%=url%>"><%=url%></a></p>
<% else %>
<p><a href="">Submit new url</a> </p>
<% end %>
</div>
Descargar Aplicación De La Entrada
Actualización 9/12/12
Uso del método Array#permutation en lugar del método Array#combination.