提供短链访问能力

序言

上一篇文章中,HTTP 服务已经可以将入参中的原始 URL 存储起来,并将其与一个唯一的 ID 绑定在一起。在本文中,将会新增一个接口,实现将数值 ID 转换为原始 URL,并通过重定向返回给调用者。

重定向到原始 URL

现有的接口/api/shorten的返回值并不利于使用,为此接下来会修改为返回Content-Type: application/json类型的数据。此外,还会有一个新接口/api/lengthen,它接受一个参数id,从内存中找出与这个 ID 绑定的原始 URL,并通过 302 的状态码返回。

:- dynamic url_to_id/2.  % 声明一个“动态谓词”。

:- initialization(main, main).

:- use_module(library(http/http_dispatch), [http_handler/3, http_redirect/3]).
:- use_module(library(http/http_parameters), [http_parameters/2]).
:- use_module(library(http/http_server), [http_server/1]).

lengthen_url(Request) :-
    http_parameters(Request, [
        id(Id, [integer])
    ]),
    url_to_id(Url, Id),
    http_redirect(moved, Url, Request).

shorten_url(Request) :-
    http_parameters(Request, [
        url(Url, [])
    ]),
    % 姑且用当前时间戳来作为 ID。
    get_time(TimeStamp),
    Id is truncate(TimeStamp),
    asserta(url_to_id(Url, Id)),  % 定义这个动态谓词的一组值。
    format('Content-Type: application/json~n~n'),
    format('{"id": ~d}', [Id]).

main(_) :-
    http_handler('/api/lengthen', lengthen_url, [methods([get])]),
    http_handler('/api/shorten', shorten_url, [methods([post])]),
    http_server([port(8082)]),
    sleep(100000000).

启动 HTTP 服务后,先请求接口/api/shorten,获得一个短链的 ID,再访问新接口即可重定向到原始 URL。

新接口的效果 新接口的效果

ID 无效怎么办

如果我捏造一个不存在的 ID 来输入,那么url_to_id(Url, Id)就无法成立,会导致接口触发异常、返回 500 的状态码。为了处理这种情况,可以定义一个新的谓词reply_by_id,它接受一个参数Id,并且只有在下列两种情况下才成立:

  1. 入参Id可以使url_to_id(Url, Id)成立,此时返回 301 的状态码。或者;
  2. 直接返回 400 的状态码。

改写后的代码如下

:- dynamic url_to_id/2.  % 声明一个“动态谓词”。

:- initialization(main, main).

:- use_module(library(http/http_dispatch), [http_404/2, http_handler/3, http_redirect/3]).
:- use_module(library(http/http_parameters), [http_parameters/2]).
:- use_module(library(http/http_server), [http_server/1]).

% 辅助函数。
reply_by_id(Id, Request) :-
    url_to_id(Url, Id),
    http_redirect(moved, Url, Request).
reply_by_id(_, Request) :-
    http_404([], Request).

lengthen_url(Request) :-
    http_parameters(Request, [
        id(Id, [integer])
    ]),
    reply_by_id(Id, Request).

shorten_url(Request) :-
    http_parameters(Request, [
        url(Url, [])
    ]),
    % 姑且用当前时间戳来作为 ID。
    get_time(TimeStamp),
    Id is truncate(TimeStamp),
    asserta(url_to_id(Url, Id)),  % 定义这个动态谓词的一组值。
    format('Content-Type: application/json~n~n'),
    format('{"id": ~d}', [Id]).

main(_) :-
    http_handler('/api/lengthen', lengthen_url, [methods([get])]),
    http_handler('/api/shorten', shorten_url, [methods([post])]),
    http_server([port(8082)]),
    sleep(100000000).

这下子如果输入一个未曾返回过的 ID,则接口将返回 404 状态码。

对无效ID的拦截 对无效ID的拦截

IF 语句

按照上文的谓词reply_by_id的定义,Prolog 程序在运行时首先会尝试该谓词的第一种情况,即

reply_by_id(Id, Request) :-
    url_to_id(Url, Id),
    http_redirect(moved, Url, Request).

如果输入的Id未曾生成过,那么查询url_to_id(Url, Id)无法成立,整个查询reply_by_id(Id, Request)的结果也会为假。接着,Prolog 会尝试谓词的第二个分支,即

reply_by_id(_, Request) :-
    http_404([], Request).

显然,这个分支总是可以成立,最终调用方收到了 404 的响应。这种非此即彼的场景,在传统语言中可以用if-else之类的语法结构来表达,在 Prolog 中其实也有等价的东西,就是谓词->/2。借助它,谓词reply_by_id可以改写为以下形式

reply_by_id(Id, Request) :-
    (  url_to_id(Url, Id)
    -> http_redirect(moved, Url, Request)
    ;  http_404([], Request)).

它的效果是相同的。