URL shortener

From Rosetta Code
URL shortener is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

The job of a URL shortener is to take a long URL (e.g. "https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html") and turn it into something shorter (e.g. "https://bit.ly/2QLUQIu").

A simple URL shortener with no special rules is very simple and consists of 2 endpoints:

  • One to generate a short version of a URL given a long version of a URL.
  • One to handle a call to the short version of a URL and redirect the user to the long (original) version of the URL.


Create a simple URL shortening API with the following endpoints:

POST /

A POST endpoint that accepts a JSON body describing the URL to shorten. Your URL shortener should generate a short version of the URL and keep track of the mapping between short and long URLs. For example:

   $ curl -X POST 'localhost:8080' \
   -H 'Content-Type: application/json' \
   -d '{
       "long": "https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html"
   }'

Should returning something similar to:

   http://localhost:8080/9eXmFnuj

GET /:short

A GET endpoint that accepts a short version of the URL in the URL path and redirects the user to the original URL. For example:

   $ curl -L http://localhost:3000/9eXmFnuj

Should redirect the user to the original URL:

   <!DOCTYPE html>
   <html lang="en">
   ...

Rules:

  • Store the short -> long mappings in any way you like. In-memory is fine.
  • There are no auth requirements. Your API can be completely open.

Crystal

Library: kemal
require "kemal"

CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".chars
entries = Hash(String, String).new

post "/" do |env|
  short = Random::Secure.random_bytes(8).map{|b| CHARS[b % CHARS.size]}.join
  entries[short] = env.params.json["long"].as(String)
  "http://localhost:3000/#{short}"
end

get "/:short" do |env|
  if long = entries[env.params.url["short"]]?
    env.redirect long
  else
    env.response.status_code = 404
  end
end

error 404 do
  "invalid short url"
end

Kemal.run

Delphi

Library: System.Json
Library: IdContext
Library: IdGlobal
Library: Inifiles

Highly inspired in #Go

program URLShortenerServer;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.Classes,
  System.Json,
  IdHTTPServer,
  IdContext,
  IdCustomHTTPServer,
  IdGlobal,
  Inifiles;

type
  TUrlShortener = class
  private
    Db: TInifile;
    Server: TIdHTTPServer;
    function GenerateKey(size: integer): string;
    procedure Get(Path: string; AResponseInfo: TIdHTTPResponseInfo);
    procedure Post(Url: string; AReqBody: TJSONValue; AResponseInfo: TIdHTTPResponseInfo);
    function PostBody(Data: TStream): TJSONValue;
    function StoreLongUrl(Url: string): string;
  public
    procedure DoGet(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo;
      AResponseInfo: TIdHTTPResponseInfo);
    procedure StartListening;
    constructor Create;
    destructor Destroy; override;
  end;

const
  Host = 'localhost:8080';
  CODE_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  SHORTENER_SEASON = 'URL_SHORTER';

{ Manager }

function TUrlShortener.GenerateKey(size: integer): string;
var
  le, i: integer;
begin
  SetLength(Result, size);
  le := Length(CODE_CHARS)-1;
  for i := 1 to size do
    Result[i] := CODE_CHARS[Random(le)+1];
end;

procedure TUrlShortener.StartListening;
begin
  Server.Active := true;
end;

function TUrlShortener.StoreLongUrl(Url: string): string;
begin
  repeat
    Result := GenerateKey(8);
  until not Db.ValueExists(SHORTENER_SEASON, Result);
  Db.WriteString(SHORTENER_SEASON, Result, Url);
  Db.UpdateFile;
end;

procedure TUrlShortener.Get(Path: string; AResponseInfo: TIdHTTPResponseInfo);
var
  longUrl: string;
begin
  if Db.ValueExists(SHORTENER_SEASON, Path) then
  begin
    longUrl := Db.ReadString(SHORTENER_SEASON, Path, '');
    AResponseInfo.ResponseNo := 302;
    AResponseInfo.Redirect(longUrl);
  end
  else
  begin
    AResponseInfo.ResponseNo := 404;
    writeln(format('No such shortened url: http://%s/%s', [host, Path]));
  end;
end;

procedure TUrlShortener.Post(Url: string; AReqBody: TJSONValue; AResponseInfo:
  TIdHTTPResponseInfo);
var
  longUrl, shortUrl: string;
begin
  if Assigned(AReqBody) then
  begin
    longUrl := AReqBody.GetValue<string>('long');
    shortUrl := StoreLongUrl(longUrl);
    AResponseInfo.ResponseNo := 200;
    AResponseInfo.ContentText := Host + '/' + shortUrl;
  end
  else
    AResponseInfo.ResponseNo := 422;
end;

function TUrlShortener.PostBody(Data: TStream): TJSONValue;
var
  body: string;
begin
  Result := nil;
  if assigned(Data) then
  begin
    Data.Position := 0;
    body := ReadStringFromStream(Data);

    result := TJSONObject.Create;
    try
      result := TJSONObject.ParseJSONValue(body);
    except
      on E: Exception do
        FreeAndNil(Result);
    end;
  end;
end;

constructor TUrlShortener.Create;
begin
  Db := TInifile.Create(ChangeFileExt(ParamStr(0), '.db'));
  Server := TIdHTTPServer.Create(nil);
  Server.DefaultPort := 8080;
  Server.OnCommandGet := DoGet;
end;

destructor TUrlShortener.Destroy;
begin
  Server.Active := false;
  Server.Free;
  Db.Free;
  inherited;
end;

procedure TUrlShortener.DoGet(AContext: TIdContext; ARequestInfo:
  TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
var
  Path: string;
begin
  // Default  ResponseNo
  AResponseInfo.ResponseNo := 404;

  Path := ARequestInfo.URI.Replace('/', '', []);

  case ARequestInfo.CommandType of
    hcGET:
      Get(Path, AResponseInfo);
    hcPOST:
      Post(Path, PostBody(ARequestInfo.PostStream), AResponseInfo);
  else
    Writeln('Unsupprted method: ', ARequestInfo.Command);
  end;
end;

var
  Server: TIdHTTPServer;
  Manager: TUrlShortener;

begin
  Manager := TUrlShortener.Create;
  Manager.StartListening;

  Writeln('Running on ', host);
  Writeln('Press ENTER to exit');
  readln;

  Manager.Free;
end.
Output:
Running on localhost:8080
Press ENTER to exit

Go

// shortener.go
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "math/rand"
    "net/http"
    "time"
)

const (
    chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    host  = "localhost:8000"
)

type database map[string]string

type shortener struct {
    Long string `json:"long"`
}

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    switch req.Method {
    case http.MethodPost: // "POST"
        body, err := ioutil.ReadAll(req.Body)
        if err != nil {
            w.WriteHeader(http.StatusBadRequest) // 400
            return
        }
        var sh shortener
        err = json.Unmarshal(body, &sh)
        if err != nil {
            w.WriteHeader(http.StatusUnprocessableEntity) // 422
            return
        }
        short := generateKey(8)
        db[short] = sh.Long
        fmt.Fprintf(w, "The shortened URL: http://%s/%s\n", host, short)
    case http.MethodGet: // "GET"
        path := req.URL.Path[1:]
        if v, ok := db[path]; ok {
            http.Redirect(w, req, v, http.StatusFound) // 302
        } else {
            w.WriteHeader(http.StatusNotFound) // 404
            fmt.Fprintf(w, "No such shortened url: http://%s/%s\n", host, path)
        }
    default:
        w.WriteHeader(http.StatusNotFound) // 404
        fmt.Fprintf(w, "Unsupprted method: %s\n", req.Method)
    }
}

func generateKey(size int) string {
    key := make([]byte, size)
    le := len(chars)
    for i := 0; i < size; i++ {
        key[i] = chars[rand.Intn(le)]
    }
    return string(key)
}

func main() {
    rand.Seed(time.Now().UnixNano())
    db := make(database)
    log.Fatal(http.ListenAndServe(host, db))
}
Output:

Sample output (abbreviated) including building and starting the server from Ubuntu 18.04 terminal and entering a valid and then an invalid shortened URL:

$ go build shortener.go

$ ./shortener &

$ curl -X POST 'localhost:8000' \
>    -H 'Content-Type: application/json' \
>    -d '{
>        "long": "https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html"
>    }'
The shortened URL: http://localhost:8000/3DOPwhRu

$ curl -L http://localhost:8000/3DOPwhRu
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">

<meta name="description" content="Learn how to use CockroachDB from a simple Go application with the Go pq driver.">

....

</html>

$ curl -L http://localhost:8000/3DOPwhRv
No such shortened url: http://localhost:8000/3DOPwhRv

JavaScript

Works with: Node.js
#!/usr/bin/env node

var mapping = new Map();

// assure num. above 36 ^ 9
var base = 101559956668416;
// 36 ^ (10 -> length of ID)
var ceil = 3656158440062976;
// these are calculated as:
//   l = desired length
//   198 > l > 1
//     -> above 198 ends up as Infinity
//     -> below 1 ends up as 0, as one would except (pun intended)
//   base = 36 ^ (l - 1)
//   ceil = 36 ^ l

require('http').createServer((req, res) => {
	if(req.url === '/') {
		// only accept POST requests as JSON to /
		if(req.method !== 'POST' || req.headers['content-type'] !== 'application/json') {
			// 400 Bad Request
			res.writeHead(400);
			return res.end();
		}

		var random = (Math.random() * (ceil - base) + base).toString(36);
		req.on('data', chunk => {
			// trusting input json to be valid, e.g., '{"long":"https://www.example.com/"}'
			var body = JSON.parse(chunk.toString());
			mapping.set(random.substring(0, 10), body.long); // substr gets the integer part
		});

		// 201 Created
		res.writeHead(201);
		return res.end('http://localhost:8080/' + random.substring(0, 10));
	}

	var url = mapping.get(req.url.substring(1));
	if(url) {
		// 302 Found
		res.writeHead(302, { 'Location': url });
		return res.end();
	}

	// 404 Not Found
	res.writeHead(404);
	res.end();
}).listen(8080);
Output:
$ curl -X POST http://localhost:8080/ -H "Content-Type: application/json" --data "{\"long\":\"https://www.example.com\"}"
http://localhost:8080/bcg4x4lla8
$ 

http://localhost:8080/bcg4x4lla8 then redirects to www.example.com

Julia

Assumes an SQLite database containing a table called LONGNAMESHORTNAME (consisting of two string columns) already exists.

using Base64, HTTP, JSON2, Sockets, SQLite, SHA

function processpost(req::HTTP.Request, urilen=8)
    json = JSON2.read(String(HTTP.payload(req)))
    if haskey(json, :long)
        longname = json.long
        encoded, shortname = [UInt8(c) for c in base64encode(sha256(longname))], ""
        for i in 0:length(encoded)-1
            shortname = String(circshift(encoded, i)[1:urilen])
            result = SQLite.Query(dbhandle, 
                "SELECT LONG FROM LONGNAMESHORTNAME WHERE SHORT = \"" * shortname * "\";")
            if isempty(result)
                SQLite.Query(dbhandle, 
                    "INSERT INTO LONGNAMESHORTNAME VALUES ('" * 
                        longname *  "', '" * shortname * "')")
                return HTTP.Response(200, JSON2.write(
                    "$shortname is short name for $longname."))
            end
        end
    end
    HTTP.Response(400, JSON2.write("Bad request. Please POST JSON as { long : longname }"))
end

function processget(req::HTTP.Request)
    shortname = split(req.target, r"[^\w\d\+\\]+")[end]
    result = SQLite.Query(dbhandle, "SELECT LONG FROM LONGNAMESHORTNAME WHERE SHORT = \'" *
            shortname * "\' ;")
    responsebody = isempty(result) ? 
        "<!DOCTYPE html><html><head></head><body><h2>Not Found</h2></body></html>" :
        "<!DOCTYPE html><html><head></head><body>\n<meta http-equiv=\"refresh\"" * 
        "content = \"0; url = " * first(result).LONG * " /></body></html>"
    return HTTP.Response(200, responsebody)
end

function run_web_server(server, portnum)
    router = HTTP.Router()
    HTTP.@register(router, "POST", "", processpost)
    HTTP.@register(router, "GET", "/*", processget)
    HTTP.serve(router, server, portnum)
end

const dbhandle = SQLite.DB("longshort.db")
const serveraddress = Sockets.localhost
const localport = 3000
run_web_server(serveraddress, localport)

Objeck

use Collection.Generic;
use Data.JSON;
use Web.HTTP;

class Shortner from HttpRequestHandler {
  @url_mapping : static : Hash<String, String>;

  function : Init() ~ Nil {
    @url_mapping := Hash->New()<String, String>;
  }

  New() {
    Parent();
  }

  function : Main(args : String[]) ~ Nil {
    if(args->Size() = 1) {
      Shortner->Init();
      WebServer->Serve(Shortner->New()->GetClass(), args[0]->ToInt(), true);
    };
  }

  method : ProcessGet(request_url : String, request_headers : Map<String, String>, response_headers : Map<String, String>) ~ Response {
    long_url := @url_mapping->Find(request_url);
    if(long_url <> Nil) {
      response := Response->New(302);
      response->SetReason(long_url);

      return response;
    };
    
    return Response->New(404);
  }
  
  method : ProcessPost(buffer : Byte[], request_url : String, request_headers : Map<String, String>, response_headers : Map<String, String>) ~ Response {
    response : Byte[];
    
    json := JsonParser->New(String->New(buffer));
    if(json->Parse()) {
      url_json := json->GetRoot()->Get("long");
      long_url := url_json->GetValue();
      
      post_fix := "/";
      each(i : 6)  {
        if(Int->Random(1) % 2 = 0) {
          post_fix += Int->Random('a', 'z')->As(Char);
        }
        else {
          post_fix += Int->Random('A', 'Z')->As(Char);
        };
      };

      short_url := "http://localhost:60013{$post_fix}";
      @url_mapping->Insert(post_fix, long_url);

      response_headers->Insert("Content-type", "application/json");
      response := "{ \"short\": \"{$short_url}\" }"->ToByteArray();
    };

    return Response->New(200, response);
  }
}

Phix

--
-- demo\rosetta\URL_shortener.exw
-- ==============================
--
-- Uses a routine originally written for a code minifier, so were you to run this 
-- for a long time, you'd get 52 one-character short urls, ie a..z and A..Z, then 
-- 3,224 (=52*62) two-character short urls, as 2nd char on can also be 0..9, then 
-- 199,888 (=52*62*62) three-character short urls, and so on. The dictionary used
-- is not [yet] saved/reloaded between runs. No attempt is made to re-produce the 
-- same short url if the same long url is passed in twice. Nothing remotely like
-- any form of error handling, as per the usual "for clarity".
--
-- Windows only for now (should be straightforward to get it working on linux)
--                      (routines in builtins\sockets.e that be windows-only.)
--
-- See sample session output (in a separate terminal) for usage instructions.
-- 
without js
include builtins\sockets.e
include builtins\json.e

constant MAX_QUEUE      = 100,
         ESCAPE         = #1B,
         shortened = substitute("""
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: %d

The shortened URL: http://localhost:8080/%s

""","\n","\r\n"),
         redirect = substitute("""
HTTP/1.1 302 Found
Content-Type: text/html; charset=UTF-8
Content-Length: %d
Location: %s

<a href="%s">Found</a>.

""","\n","\r\n"),
         not_found = substitute("""
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Content-Length: %d

No such shortened url: http://localhost:8080/%s

""","\n","\r\n")

integer shurl = new_dict()
string response, lnk, url

constant alphabet = tagset('z','a')&tagset('Z','A')&tagset('9','0')
function short_id(integer n)
    string res = ""
    integer base = 52 -- (first char azAZ)
    while n do
        res &= alphabet[remainder(n-1,base)+1]
        n = floor((n-1)/base)
        base = 62 -- (subsequent chars azAZ09)
    end while
    return res
end function

puts(1,"server started, open http://localhost:8080/ in browser or curl, press Esc or Q to quit\n")
atom sock = socket(AF_INET,SOCK_STREAM,NULL),
     pSockAddr = sockaddr_in(AF_INET, "", 8080)
if bind(sock, pSockAddr)=SOCKET_ERROR then crash("bind (%v)",{get_socket_error()}) end if
if listen(sock,MAX_QUEUE)=SOCKET_ERROR then crash("listen (%v)",{get_socket_error()}) end if
while not find(get_key(),{ESCAPE,'q','Q'}) do
    {integer code} = select({sock},{},{},250000)
    if code=SOCKET_ERROR then crash("select (%v)",{get_socket_error()}) end if
    if code>0 then  -- (not timeout)
        atom peer = accept(sock),
             ip = getsockaddr(peer)
        {integer len, string request} = recv(peer)
        printf(1,"Client IP: %s\n%s\n",{ip_to_string(ip),request})
        if length(request)>5 and request[1..5]="POST " then
            string json = request[find('{',request)..$]
            object json_data = parse_json(json)
            url = extract_json_field(json_data,"long")
            lnk = short_id(dict_size(shurl)+1)
            setd(lnk,url,shurl)
            response = sprintf(shortened,{length(lnk)+45,lnk})
        elsif length(request)>4 and request[1..4]="GET " then
            lnk = request[6..find(' ',request,6)-1]
            integer node = getd_index(lnk,shurl)
            if node then
                url = getd_by_index(node,shurl)
                response = sprintf(redirect,{length(url)+23,url,url})
            else
                response = sprintf(not_found,{length(lnk)+49,lnk})
            end if
        else
            ?9/0 -- uh?
        end if
        integer bytes_sent = send(peer,response)
        printf(1,"%d bytes successfully sent\n",bytes_sent)
        shutdown(peer, SD_SEND) -- tell curl it's over
        peer = closesocket(peer)  --    (as does this)
    end if
end while
sock = closesocket(sock)
WSACleanup()
Output:

Sample session output:

C:\Program Files (x86)\Phix>curl http://localhost:8080/X
No such shortened url: http://localhost:8000/X

C:\Program Files (x86)\Phix>curl -X POST "localhost:8080" -H "Content-Type: application/json" -d "{\"long\":\"https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html\"}"
The shortened URL: http://localhost:8000/a

C:\Program Files (x86)\Phix>curl http://localhost:8080/a
<a href="https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html">Found</a>.

Of course if you paste http://localhost:8080/a into a browser (with URL_shortener.exw still running!) it goes to the right place.
Meanwhile, in a separate terminal (with * replaced by $ to avoid comment issues) we get:

server started, open http://localhost:8080/ in browser or curl, press Esc or Q to quit
Client IP: 127.0.0.1
GET /a HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.55.1
Accept: $/$


137 bytes successfully sent
Client IP: 127.0.0.1
POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.55.1
Accept: $/$
Content-Type: application/json
Content-Length: 89

{"long":"https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html"}
126 bytes successfully sent
Client IP: 127.0.0.1
GET /a HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.55.1
Accept: $/$


276 bytes successfully sent

PicoLisp

(load "@lib/http.l")
(allowed NIL "!short" u)
(pool "urls.db" (6))
(de short (R)
   (ifn *Post
      (redirect (fetch NIL (format R)))
      (let K (count)
         (dbSync)
         (store NIL K (get 'u 'http))
         (commit 'upd)
         (respond (pack "http://127.0.0.1:8080/?" K "\n")) ) ) )
(server 8080 "!short")
Output:
$ nohup pil url-short.l +
$ curl -F 'u=https://reddit.com' 127.0.0.1:8080
http://127.0.0.1:8080/?0
$ curl -F 'u=https://picolisp.com' 127.0.0.1:8080
http://127.0.0.1:8080/?1
$ curl -v http://127.0.0.1:8080/?1
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /?1 HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.69.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 303 See Other
< Server: PicoLisp
< Location: https://picolisp.com
< Content-Type: text/html
< Content-Length: 89
< 
<HTML>
<HEAD><TITLE>303 See Other</TITLE></HEAD>
<BODY><H1>See Other</H1></BODY>
</HTML>
* Connection #0 to host 127.0.0.1 left intact
$

Python

Flask

Library: Flask
"""A URL shortener using Flask. Requires Python >=3.5."""

import sqlite3
import string
import random

from http import HTTPStatus

from flask import Flask
from flask import Blueprint
from flask import abort
from flask import current_app
from flask import g
from flask import jsonify
from flask import redirect
from flask import request
from flask import url_for


CHARS = frozenset(string.ascii_letters + string.digits)
MIN_URL_SIZE = 8
RANDOM_ATTEMPTS = 3


def create_app(*, db=None, server_name=None) -> Flask:
    app = Flask(__name__)
    app.config.from_mapping(
        DATABASE=db or "shorten.sqlite",
        SERVER_NAME=server_name,
    )

    with app.app_context():
        init_db()

    app.teardown_appcontext(close_db)
    app.register_blueprint(shortener)

    return app


def get_db():
    if "db" not in g:
        g.db = sqlite3.connect(current_app.config["DATABASE"])
        g.db.row_factory = sqlite3.Row

    return g.db


def close_db(_):
    db = g.pop("db", None)

    if db is not None:
        db.close()


def init_db():
    db = get_db()

    with db:
        db.execute(
            "CREATE TABLE IF NOT EXISTS shorten ("
            "url TEXT PRIMARY KEY, "
            "short TEXT NOT NULL UNIQUE ON CONFLICT FAIL)"
        )


shortener = Blueprint("shorten", "short")


def random_short(size=MIN_URL_SIZE):
    """Return a random URL-safe string `size` characters in length."""
    return "".join(random.sample(CHARS, size))


@shortener.errorhandler(HTTPStatus.NOT_FOUND)
def short_url_not_found(_):
    return "short url not found", HTTPStatus.NOT_FOUND


@shortener.route("/<path:key>", methods=("GET",))
def short(key):
    db = get_db()

    cursor = db.execute("SELECT url FROM shorten WHERE short = ?", (key,))
    row = cursor.fetchone()

    if row is None:
        abort(HTTPStatus.NOT_FOUND)

    # NOTE: Your might want to change this to HTTPStatus.MOVED_PERMANENTLY
    return redirect(row["url"], code=HTTPStatus.FOUND)


class URLExistsError(Exception):
    """Exception raised when we try to insert a URL that is already in the database."""


class ShortCollisionError(Exception):
    """Exception raised when a short URL is already in use."""


def _insert_short(long_url, short):
    """Helper function that checks for database integrity errors explicitly
    before inserting a new URL."""
    db = get_db()

    if (
        db.execute("SELECT * FROM shorten WHERE url = ?", (long_url,)).fetchone()
        is not None
    ):
        raise URLExistsError(long_url)

    if (
        db.execute("SELECT * FROM shorten WHERE short = ?", (short,)).fetchone()
        is not None
    ):
        raise ShortCollisionError(short)

    with db:
        db.execute("INSERT INTO shorten VALUES (?, ?)", (long_url, short))


def make_short(long_url):
    """Generate a new short URL for the given long URL."""
    size = MIN_URL_SIZE
    attempts = 1
    short = random_short(size=size)

    while True:
        try:
            _insert_short(long_url, short)
        except ShortCollisionError:
            # Increase the short size if we keep getting collisions.
            if not attempts % RANDOM_ATTEMPTS:
                size += 1

            attempts += 1
            short = random_short(size=size)
        else:
            break

    return short


@shortener.route("/", methods=("POST",))
def shorten():
    data = request.get_json()

    if data is None:
        abort(HTTPStatus.BAD_REQUEST)

    long_url = data.get("long")

    if long_url is None:
        abort(HTTPStatus.BAD_REQUEST)

    db = get_db()

    # Does this URL already have a short?
    cursor = db.execute("SELECT short FROM shorten WHERE url = ?", (long_url,))
    row = cursor.fetchone()

    if row is not None:
        short_url = url_for("shorten.short", _external=True, key=row["short"])
        status_code = HTTPStatus.OK
    else:
        short_url = url_for("shorten.short", _external=True, key=make_short(long_url))
        status_code = HTTPStatus.CREATED

    mimetype = request.accept_mimetypes.best_match(
        matches=["text/plain", "application/json"], default="text/plain"
    )

    if mimetype == "application/json":
        return jsonify(long=long_url, short=short_url), status_code
    else:
        return short_url, status_code


if __name__ == "__main__":
    # Start the development server
    app = create_app()
    app.env = "development"
    app.run(debug=True)

Raku

(formerly Perl 6)

Works with: Rakudo version 2019.11

As there is no requirement to obfuscate the shortened urls, I elected to just use a simple base 36 incremental counter starting at "0" to "shorten" the urls.

Sets up a micro web service on the local computer at port 10000. Run the service, then access it with any web browser at address localhost:10000 . Any saved urls will be displayed with their shortened id. Enter a web address in the text field to assign a shortened id. Append that id to the web address to automatically be redirected to that url. The url of this page is entered as id 0, so the address: " localhost:10000/0 " will redirect to here, to this page.

The next saved address would be accessible at localhost:10000/1 . And so on.

Saves the shortened urls in a local json file called urls.json so saved urls will be available from session to session. No provisions to edit or delete a saved url. If you want to edit the saved urls, edit the urls.json file directly with some third party editor then restart the service.

There is NO security or authentication on this minimal app. Not recommended to run this as-is on a public facing server. It would not be too difficult to add appropriate security, but it isn't a requirement of this task. Very minimal error checking and recovery.

# Persistent URL storage
use JSON::Fast;

my $urlfile = './urls.json'.IO;
my %urls = ($urlfile.e and $urlfile.f and $urlfile.s) ??
  ( $urlfile.slurp.&from-json ) !!
  ( index => 1, url => { 0 => 'http://rosettacode.org/wiki/URL_shortener#Raku' } );

$urlfile.spurt(%urls.&to-json);

# Setup parameters
my $host = 'localhost';
my $port = 10000;

# Micro HTTP service
use Cro::HTTP::Router;
use Cro::HTTP::Server;

my $application = route {
    post -> 'add_url' {
        redirect :see-other, '/';
        request-body -> (:$url) {
            %urls<url>{ %urls<index>.base(36) } = $url;
            ++%urls<index>;
            $urlfile.spurt(%urls.&to-json);
        }
    }

    get -> {
        content 'text/html', qq:to/END/;
        <form action="http://$host:$port/add_url" method="post">
        URL to add:</br><input type="text" name="url"></br>
        <input type="submit" value="Submit"></form></br>
        Saved URLs:
        <div style="background-color:#eeeeee">
        { %urls<url>.sort( +(*.key.parse-base(36)) ).join: '</br>' }
        </div>
        END
    }

    get -> $short {
        if my $link = %urls<url>{$short} {
            redirect :permanent, $link
        }
        else {
            not-found
        }
    }
}

my Cro::Service $shorten = Cro::HTTP::Server.new:
    :$host, :$port, :$application;

$shorten.start;

react whenever signal(SIGINT) { $shorten.stop; exit; }

Wren

Translation of: Go
Library: WrenGo
Library: Wren-json

An embedded program with a Go host so we can use its net/http module.

/* URL_shortener.wren */

import "./json" for JSON
import "random" for Random

var Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

var MethodPost = "POST"
var MethodGet = "GET"
var StatusBadRequest = 400
var StatusFound = 302
var StatusNotFound = 404

var Db = {}
var Rand = Random.new()

var GenerateKey = Fn.new { |size|
    var key = List.filled(size, null)
    var le = Chars.count
    for (i in 0...size) key[i] = Chars[Rand.int(le)]
    return key.join()
}

class ResponseWriter {
    foreign static writeHeader(statusCode)
    foreign static fprint(str)
}

class Request {
    foreign static method
    foreign static body
    foreign static urlPath
}

class Http {
    foreign static host

    static serve() {
        if (Request.method == MethodPost) {
            var body = Request.body
            var sh = JSON.parse(body)["long"]
            var short = GenerateKey.call(8)
            Db[short] = sh
            ResponseWriter.fprint("The shortened URL: http://%(host)/%(short)\n")
        } else if (Request.method == MethodGet) {
            var path = Request.urlPath[1..-1]
            if (Db.containsKey(path)) {
                redirect(Db[path], StatusFound)
            } else {
                ResponseWriter.writeHeader(StatusNotFound)
                ResponseWriter.fprint("No such shortened url: http://%(host)/%(path)\n")
            }
        } else {
            ResponseWriter.writeHeader(StatusNotFound)
            ResponseWriter.fprint("Unsupported method: %(Request.method)\n")
        }
    }

    foreign static redirect(url, code)
}

We now embed this script in the following Go program and build it.

/* go build URL_shortener.go */

package main

import (
    "fmt"
    wren "github.com/crazyinfin8/WrenGo"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
)

type any = interface{}

var fileName = "URL_shortener.wren"
var host = "localhost:8000"

var vm *wren.VM

var gw http.ResponseWriter
var greq *http.Request

func serveHTTP(w http.ResponseWriter, req *http.Request) {
    gw, greq = w, req
    wrenVar, _ := vm.GetVariable(fileName, "Http")
    wrenClass, _ := wrenVar.(*wren.Handle)
    defer wrenClass.Free()
    wrenMethod, _ := wrenClass.Func("serve()")
    defer wrenMethod.Free()
    wrenMethod.Call()
}

func writeHeader(vm *wren.VM, parameters []any) (any, error) {
    statusCode := int(parameters[1].(float64))
    gw.WriteHeader(statusCode)
    return nil, nil
}

func fprint(vm *wren.VM, parameters []any) (any, error) {
    str := parameters[1].(string)
    fmt.Fprintf(gw, str)
    return nil, nil
}

func method(vm *wren.VM, parameters []any) (any, error) {
    res := greq.Method
    return res, nil
}

func body(vm *wren.VM, parameters []any) (any, error) {
    res, _ := ioutil.ReadAll(greq.Body)
    return res, nil
}

func urlPath(vm *wren.VM, parameters []any) (any, error) {
    res := greq.URL.Path
    return res, nil
}

func getHost(vm *wren.VM, parameters []any) (any, error) {
    return host, nil
}

func redirect(vm *wren.VM, parameters []any) (any, error) {
    url := parameters[1].(string)
    code := int(parameters[2].(float64))
    http.Redirect(gw, greq, url, code)
    return nil, nil
}

func moduleFn(vm *wren.VM, name string) (string, bool) {
    if name != "meta" && name != "random" && !strings.HasSuffix(name, ".wren") {
        name += ".wren"
    }
    return wren.DefaultModuleLoader(vm, name)
}

func main() {
    cfg := wren.NewConfig()
    cfg.LoadModuleFn = moduleFn
    vm = cfg.NewVM()

    responseWriterMethodMap := wren.MethodMap{
        "static writeHeader(_)": writeHeader,
        "static fprint(_)":      fprint,
    }

    requestMethodMap := wren.MethodMap{
        "static method":  method,
        "static body":    body,
        "static urlPath": urlPath,
    }

    httpMethodMap := wren.MethodMap{
        "static host":          getHost,
        "static redirect(_,_)": redirect,
    }

    classMap := wren.ClassMap{
        "ResponseWriter": wren.NewClass(nil, nil, responseWriterMethodMap),
        "Request":        wren.NewClass(nil, nil, requestMethodMap),
        "Http":           wren.NewClass(nil, nil, httpMethodMap),
    }

    module := wren.NewModule(classMap)
    vm.SetModule(fileName, module)
    vm.InterpretFile(fileName)
    http.HandleFunc("/", serveHTTP)
    log.Fatal(http.ListenAndServe(host, nil))
    vm.Free()
}
Output:

Sample output (abbreviated) including building and starting the server from Ubuntu 20.04 terminal and entering a valid and then an invalid shortened URL:

$ go build URL_shortener.go

$ ./URL_shortener &

$ curl -X POST 'localhost:8000'/ \
>    -H 'Content-Type: application/json' \
>    -d '{
>        "long": "https://www.cockroachlabs.com/docs/stable/build-a-go-app-with-cockroachdb.html"
>    }'
The shortened URL: http://localhost:8000/RRyYg8tK

$ curl -L http://localhost:8000/RRyYg8tK
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Learn how to use CockroachDB from a simple Go application with the Go pgx driver.">

....

</html>

$ curl -L http://localhost:8000/3DOPwhRv
No such shortened url: http://localhost:8000/3DOPwhRv