Criando aplicações RESTful com Qt e Cutelyst

Esse mini tutorial tem como objetivo mostar os fundamentos da criação de uma aplicação RESTful com Qt, como cliente e como servidor com o auxilio do Cutelyst.

Serviços com APIs REST se tornaram muito populares nos ultimos anos, e interagir com elas pode ser necessário para integrar com serviços e manter sua aplicação relevante, assim como pode ser interessante substituir seu protocolo próprio com uma implementação REST.

REST é muito associado a JSON, porém, para um serviço se caracterizar REST não é necessário utilizar JSON, a forma como os dados são trocados é escolhida por quem define a API, ou seja é possível ter REST trocando mensagens em XML ou outro formato. Iremos utilizar JSON por sua popularidade, simplicidade e devido a classe QJsonDocument ser presente no módulo Core do Qt.

Um serviço REST é caracterizado principalmente por fazer uso dos métodos e cabeçalhos (headers) pouco utilizados do HTTP, navegadores utilizam basicamente GET para obter dados e POST para enviar dados de formulários e arquivos, porém clientes REST farão uso de métodos como DELETE, PUT e HEAD, se tratando de cabeçalhos muitas APIs definem cabeçalhos para autenticação, por exemplo X-Applicativo-Token pode conter uma chave gerada apenas para o aplicativo de um usuário X, de forma que se esse cabeçalho não conter os dados corretos ele não terá acesso aos dados.

Vamos começar definindo a API do servidor:

  • /api/v1/usuarios
    • GET - Obtém a lista de usuários
      • Resposta: [ "uuid1", "uuid2" ]
    • POST - Cadastra novo usuário
      • Envie: { "nome": "fulano", "idade": 32 }
      • Resposta: { "status": "ok/erro", "uuid": "uuid do novo usuário", "erro": "msg em caso de erro" }
  • /api/v1/usuarios/<UUID> - onde UUID deve ser substituido pelo UUID do usuário
    • GET - Obtém informações do usuário
      • Resposta: { "nome": "fulano", "idade": 32 }
    • PUT - Atualiza informações do usuário
      • Envie: { "nome": "fulano de tal", "idade": 57 }
      • Resposta: { "status": "ok/erro", "erro": "msg em caso de erro" }
    • DELETE - Apaga o usuário
      • Resposta: { "status": "ok/erro",  "erro": "msg em caso de erro" }

Por motivos de simplicidade iremos armazenar os dados utilizando QSettings, não recomendo para aplicações reais, mas foge do escopo utilizar Sql ou algo do tipo. Também assumimos que já tem o Qt e o Cutelyst instalados, o código estará disponível em https://github.com/ceciletti/example-qt-cutelyst-rest

Parte 1 - Servidor RESTful com C++, Cutelyst e Qt

Primeiramente criamos a aplicação do servidor:

$ cutelyst --create-app ServidorREST

E então iremos criar o Controller que terá os metodos da API:

$ cutelyst --controller ApiV1

Feito isso a nova classe deve ser instanciada em serverrest.cpp, no método init() com, 

#include "apiv1.h"

bool ServerREST::init() {

    new ApiV1(this);

    ...

Adicione os seguintes métodos ao arquivo "apiv1.h"

C_ATTR(usuarios, :Local :AutoArgs :ActionClass(REST))
void usuarios(Context *c);

C_ATTR(usuarios_GET, :Private)
void usuarios_GET(Context *c);

C_ATTR(usuarios_POST, :Private)
void usuarios_POST(Context *c);

C_ATTR(usuarios_uuid, :Path('usuarios') :AutoArgs :ActionClass(REST))
void usuarios_uuid(Context *c, const QString &uuid);

C_ATTR(usuarios_uuid_GET, :Private)
void usuarios_uuid_GET(Context *c, const QString &uuid);

C_ATTR(usuarios_uuid_PUT, :Private)
void usuarios_uuid_PUT(Context *c, const QString &uuid);

C_ATTR(usuarios_uuid_DELETE, :Private)
void usuarios_uuid_DELETE(Context *c, const QString &uuid);

A macro C_ATTR é usada para adicionar metadados sobre a classe que o MOC ira guardar, dessa forma o Cutelyst sabe como mapear as URLs a essas funções.

  • :Local - Mapeia o nome do metodo a URL gerando /api/v1/usuarios
  • :AutoArgs - Verifica automaticamente o número de argumentos após o Context*, em usuários_uuid temos um logo o método será chamado caso a URL seja /api/v1/usuarios/qualquer-coisa
  • :ActionClass(REST) - Ira carregar o plugin REST que criará uma classe Action para tomar conta desse método, ActionREST ira chamar os outros métodos dependendo do método chamado
  • :Private - Registra a ação como privada no Cutelyst, de forma que ela não é diretamente acessível via UR

Pronto isso já é suficiente para termos um mapeamento automático dependendo do método HTTP para cada função, é importante notar que a primeira função (sem _MÉTODO) é sempre executada, para mais informações veja a API do ActionREST

Por brevidade vou mostrar apenas o código do GET de usuários, o restante pode ser visto no GitHub:

void ApiV1::usuarios_GET(Context *c)
{
    QSettings s;
    const QStringList uuids = s.childGroups();

    QJsonArray array;
    for (const QString &uuid : uuids) {
        array.append(uuid);
    }

    c->response()->setJsonBody(array);
}

Após todos os métodos implementados inicie o servidor:

cutelyst -r --server

Para testar a API pode testar um POST com o curl:

curl -H "Content-Type: application/json" -X POST -d '{"nome":"fulano","idade":32}' http://localhost:3000/api/v1/usuarios

Pronto, você já tem uma aplicação servidor REST, feita em Qt, com uma das mais respostas mais rápidas do velho oeste :)

Não, é serio, confira os benchmarks.

Agora vamos para parte 2, que é criar a aplicação cliente que ira consumir essa API.

Parte 2 - Aplicação cliente REST

Primeiramente crie um projeto QWidgets com uma QMainWindow, o objetivo aqui é apenas ver como criar as solicitações REST a partir de código Qt, então assumimos que já tem familiaridade em criar interfaces gráficas com o mesmo.

Nossa interface será composta:

  • 1 - QComboBox onde listaremos os UUIDs dos usuários
  • 1 - QLineEdit para digitar e visualizar o nome do usuário
  • 1 - QSpinBox para digitar e visualizar a idade do usuário
  • 2 - QPushButton
    • Para criar ou atualizar o registro de um usuário
    • Para apagar o registro do usuário

Desenhada a interface, nossa sub-classe do QMainWindow precisa ter um ponteiro para o QNetworkAccessManager, essa é a classe responsável por cuidar da comunicação com serviços de rede como HTTP e FTP. Essa classe trabalha de forma assíncrona, ela tem o funcionamento similar ao de um navegador que irá criar até 6 conexões simultâneas a um mesmo servidor, caso você tenha feito mais requisições ao mesmo tempo ela ira colocar as mesmas em uma fila.

Crie então um ponteiro  QNetworkAccessManager *m_nam; como membro de sua classe para podermos reutiliza-lo. A nossa requisição para obter a lista de usuários será bastante simples:

QNetworkRequest request(QUrl("http://localhost:3000/api/v1/usuarios"));

QNetworkReply *reply = m_nam->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply] () {
    reply->deleteLater();
    const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
    const QJsonArray array = doc.array();

    for (const QJsonValue &value : array) {
        ui->uuidCB->addItem(value.toString());
    }
});

Pronto já preenchemos com os dados via GET do servidor nosso QComboBox, agora vamos ver o código do cadastramento que é um pouco mais complexo:

QNetworkRequest request(QUrl("http://localhost:3000/api/v1/usuarios"));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");

QJsonObject obj{
    {"nome", ui->nomeLE->text()},
    {"idade", ui->idadeSP->value()}
};

QNetworkReply *reply = m_nam->post(request, QJsonDocument(obj).toJson());
connect(reply, &QNetworkReply::finished, this, [this, reply] () {
    reply->deleteLater();
    const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
    const QJsonObject obj = doc.object();

    if (obj.value("status").toString() == "ok") {
        ui->uuidCB->addItem(obj.value("uuid").toString());
    } else {
        qWarning() << "ERROR" << obj.value("error").toString();
    }
});

Com o código acima enviamos uma requisição HTTP usando o método POST, esse por sua vez assim como PUT aceita enviar dados para o servidor. É fundamental informar ao servidor com que tipo de dados ele estará lidando, para isso o cabeçalho "Content-Type" esta definido para "application/json", o Qt emite um aviso no terminal caso o tipo de conteúdo não tenha sido definido. Assim que o servidor responde nós adicionamos o novo UUID no combobox para que ele fique atualizado sem precisar obter todos os UUID novamente.

Como demonstrado QNetworkAccessManager já tem métodos prontos para as ações REST mais comuns, porém caso queria enviar um pedido do tipo OPTIONS por exemplo terá que criar um pedido do tipo CustomOperation:

m_nam->sendCustomRequest("OPTIONS", request);

Gostou do artigo? Ajude dando uma estrelinha para o Cutelyst https://github.com/cutelyst/cutelyst