Antes que você se irrite, ouça-me. O CMS (sistema de gerenciamento de conteúdo) não precisa ter um painel de administração inflado. Esta é a história de como escrevi um CMS em Rust.
Introdução
Assim, o CMS é algo que sempre gostei de usar. Gosto da ideia de ter um serviço que controla seu serviço. Quero dizer, é como um service-ception. Mas sempre detestei a ideia de ter um painel de administração inflado. Quero dizer, por que você precisa de um painel de administração completo para um blog que pode ser gerenciado com uma ferramenta CLI (Interface de linha de comando)? Se você quiser, pode, mas por que não integrá-lo ao seu blog? Se você fizer isso, precisará fazer muitas coisas:
- Necessidade de configurar rotas seguras
- Adicionar autenticação
- Gerenciar o Controle de Acesso
- Basicamente, muitas coisas
Então, pensei: por que não escrever uma ferramenta CLI que cuide de tudo isso para você e, como eu estava aprendendo Rust, por que não escrevê-la em Rust? Portanto, dê boas-vindas ao Noob Handler.
Por Que Escrever em Rust?
Basicamente, adoro as ferramentas CLI. Elas são uma maneira excelente e fácil de fazer as coisas. Também são muito fáceis de enviar. Não há necessidade de verificar a compatibilidade com o Windows ou algo do gênero. Basta compilá-las e enviá-las. É isso aí. Além disso, o Rust é uma ótima linguagem para escrever ferramentas CLI. É rápida, fácil de escrever e fácil de enviar. Você obtém um executável na hora. Além disso, você obtém um executável que é rápido e de tamanho pequeno. Não há nenhuma pasta de dependências estranha que precise ser enviada ou algo do gênero.
Por Que Não o Python?
O Python, por outro lado, pelo menos na minha opinião, tem a pior experiência executável. Para compilar um arquivo python, você pode acabar usando uma biblioteca como o pyinstaller
ou algo assim. E, mesmo assim, você acabaria com um executável enorme. Quero dizer, não quero enviar um executável de 100 MB para uma ferramenta CLI. Isso é demais. Além disso, vamos dar uma olhada em como o pyinstaller
funciona.
Como você pode ver, o pyinstaller
funciona principalmente criando um ambiente virtual e, em seguida, instalando todas as dependências nesse ambiente virtual. Em seguida, ele cria todos os vínculos necessários para todas as dependências no cpython. E, a seguir, ele cria um executável. Esse executável é apenas para fins de nome. É apenas um invólucro que envolve o ambiente virtual. Portanto, quando você roda o executável, ele apenas executa o ambiente virtual e, em seguida, o arquivo Python. Isso torna o tempo de execução mais lento e também aumenta o tamanho do executável. Isso, a longo prazo, não é bom para uma ferramenta CLI, o que nos leva a uma conclusão importante.
O Python não foi feito para ser compilado, mas para ser interpretado.
Não me entenda mal, o Python tem essas bibliotecas legais, como a rich
, que facilitam muito a criação de ferramentas CLI em Python. Já trabalhei com a rich
no passado e ela é uma das minhas bibliotecas favoritas. No entanto, ela é melhor quando usada como um script. Eu uso a rich
para personalizar meus scripts e ela funciona muito bem. Mas, quando se trata de escrever uma ferramenta CLI, prefiro usar uma linguagem feita para ser compilada.
O Rust é incrível
Então, bem-vindo ao Rust. O Rust tem várias bibliotecas que facilitam a criação de ferramentas CLI. Você tem uma para fazer perguntas, uma para personalizar, uma para analisar argumentos e assim por diante. Ele é o Python com a velocidade do C e a segurança do Haskell. O que mais você pode pedir? De fato, esqueça isso. Você não pode realmente comparar o Rust com qualquer outra linguagem. Ele é uma linguagem própria e é excelente no que faz.
Aqui estão algumas coisas que me fizeram escolher o Rust:
- Ótimo Sistema de Tipagem
- Gerenciador de pacotes simples de usar
- Bibliotecas bem documentadas para ferramentas CLI
- Executável binário único
- Execução rápida
- Bom tratamento de erros
- Ótima comunidade
Se você acha que acabei de listar os motivos mais genéricos para usar o Rust, você está certo. Quero dizer, há uma razão pela qual tantas CLIs são escritas em Rust. De fato, há uma comunidade inteira para reescrever ferramentas GNU/Linux
em Rust. Assim, por exemplo, você já deve ter ouvido falar do bat
, que é um substituto do cat
. Ele tem alguns recursos interessantes, como realce de sintaxe e coisas do gênero, e você tem o exa
, que substitui o ls
com alguns recursos interessantes, como ícones. Basicamente, você tem uma subcomunidade no Rust que está interessada em ferramentas de linha de comando.
O que é CMS?
Bem, se você não estiver familiarizado com o termo CMS, ele significa Sistema de Gerenciamento de Conteúdo. Portanto, a ideia por trás disso é que, em vez de o conteúdo ser alterado no momento da implantação, ele é alterado no tempo da execução. Deixe-me explicar. Então, digamos que você use o Astro para criar seu site. Você tem um monte de arquivos markdown que usa para gerar o site. Agora, digamos que você queira adicionar uma nova postagem. Você teria que adicionar um novo arquivo markdown e, em seguida, implantar o site. Essa é a maneira tradicional de fazer as coisas. No entanto, com um CMS, você pode simplesmente adicionar uma nova postagem no painel de administração e ela será adicionada ao site. Isso ocorre porque seu site estará lendo um banco de dados remoto e não um arquivo markdown local.
Agora, meu blog atual é escrito usando arquivos markdown. Tenho vários arquivos markdown que são executados por meio de alguns plug-ins remark e depois convertidos em HTML pelo Astro. Nada sofisticado. Isso ocorre porque as postagens do meu blog geralmente levam muito tempo para serem escritas e eu não as escrevo com muita frequência. Portanto, quando escrevo, apenas adiciono um arquivo markdown e depois implanto meu site. Não é um grande problema.
Entretanto, tenho uma página chamada quips
, na qual escrevo pequenas atualizações sobre minha vida. Eu as escrevo com bastante frequência e não quero implantar meu site toda vez que escrevo uma nova história. Por isso, uso o banco de dados MongoDB para armazenar minhas quips
. Tenho uma API simples que uso para adicionar novas quips
e, em seguida, tenho uma página que busca todos as quips
do banco de dados e as exibe. Esse é um CMS. Estou gerenciando meu conteúdo a partir de um banco de dados remoto.
Além das quips
, tenho duas outras páginas que usam um banco de dados.
- NoobPaste Mini: Um pastebin simples e pequeno que uso para compartilhar trechos de código.
- NoobShort: Um encurtador de URL simples que eu uso para encurtar URLs.
Ambos usam um banco de dados para armazenar seus dados. Eu tenho uma API simples que uso para adicionar novas pastas e novos URLs. Na verdade, tenho uma CLI do NoobShort que você pode conferir em newtoallofthis123/short_cli. É muito fácil de usar e é uma das primeiras CLIs que escrevi em Rust. Você pode instalá-la usando cargo install nshrt
e depois usá-la para encurtar URLs.
De qualquer forma, voltando ao assunto em questão, tenho várias páginas que usam um banco de dados para armazenar seus dados. Tudo isso é armazenado usando um banco de dados MongoDB. Eu uso o MongoDB porque ele é fácil de usar e de ser configurado. Portanto, o MongoDB tem um driver Rust muito fácil de usar chamado mongodb
. Ele está bem documentado e é fácil de usar. Eu o usei no passado e não tive problemas com ele. Por isso, decidi usá-lo em meu CMS.
O Plano
O plano é simples. Quero escrever uma ferramenta CLI que possa ser usada para gerenciar meu conteúdo. Tive de gerenciar três tipos de conteúdo com os seguintes esquemas B-JSON:
// Quips
{
"_id": "ObjectId",
"hash": "String",
"name": "String",
"content": "String",
"date": "String",
"author": "String",
}
// Código
{
"_id": "ObjectId",
"hash": "String",
"title": "String",
"content": "String",
"lang": "String",
"author": "String",
}
// GO
{
"_id": "ObjectId",
"url": "String",
"slug": "String",
}
Portanto, a ideia era que cada um deles apresentasse as seguintes operações:
- Show (Mostrar)
- Add (Adicionar)
- Edit (Editar)
- Excluir as operações CRUD (criar, ler, atualizar e excluir), se possível. A CLI teria os seguintes comandos:
handler __doc__ __hash__
handler new __doc__
handler list __doc__
O doc pode ser quips
, code
, ou go
. O__hash__
é o hash do documento que você deseja editar ou excluir.
A lista era basicamente uma combinação de show (exibição) e search (pesquisa). Ela mostrava todos os documentos e, em seguida, você podia pesquisar um documento específico. Isso poderia ser feito nativamente usando um crate inquire
, mas falaremos mais sobre isso posteriormente. (NT. o crate é o termo da linguagem Rust utilizado para um binário ou uma biblioteca e/ou o software livre integrado).
Resumo do Plano
Então, vamos resumir o plano:
- Conectar ao MongoDB
- Habilitar operações no banco de dados CRUD
- Adicionar uma interface CLI às operações CRUD
- Adicionar as operações New, Edit e Delete
- Adicionar uma operação list
- Tornar tudo o mais nativo e rápido possível
- Deixar o visual bonito
Vamos encarar tudo isso de frente e ver como podemos implementá-lo.
Resolvendo os Problemas
O primeiro passo foi realmente configurar o banco de dados. Eu já tinha um banco de dados com as collections (coleções) fornecidas. Simplesmente usei o URI dele e me conectei a ele usando o crate mongodb
. Foi muito fácil fazer isso. A aparência é mais ou menos essa:
async fn get_connection() -> Result<Client, mongodb::error::Error> {
let client = Client::with_uri_str(get_mongo_url()).await?;
Ok(client)
}
A função get_mongo_url
retorna apenas o URI do banco de dados. Eu o armazeno em uma variável de ambiente para não precisar usar hardcode no código. Eu apenas uso o crate dotenv
para carregar as variáveis de ambiente.
O próximo passo foi habilitar as operações CRUD no banco de dados. Isso também foi muito fácil de fazer. Portanto, o crate mongodb
tem uma struct Collection
que pode ser usada para interagir com o banco de dados. Eu predefino as coleções e, em seguida, uso apenas a struct Collection
para interagir com o banco de dados.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Page{
pub _id: ObjectId,
pub hash: String,
pub name: String,
pub content: String,
pub date: String,
pub author: String,
}
As coleções usam serde
para, automaticamente serializar e desserializar os dados. A seguir, posso usar as funções integradas da struct Collection como insert_one
, find_one
, find
, delete_one
e update_one
para interagir com o banco de dados.
pub async fn get_page(hash: &str)->Page{
let collection = get_page_conn().await;
let result = collection.find_one(doc! {"hash": hash}, None).await.unwrap();
result.expect("Page Not Found")
}
Portanto, essa é uma das funções que uso para obter uma página do banco de dados. Eu simplesmente uso a função find_one
para encontrar uma página com o hash fornecido. A macro doc!
é muito útil e é usada para criar um documento BSON. É basicamente um objeto JSON usado para consultar o banco de dados. Se você está começando a ver um padrão aqui, você está certo. O driver mongodb para Rust é basicamente um ORM para MongoDB. Uma coisa que esqueci de mencionar é que tudo isso foi configurado com tokio e futures para que a execução inteira seja bloqueada até que a operação do banco de dados seja concluída. Isso facilita a prevenção de erros de tipagem e coisas do gênero.
CLI e Inquire
O próximo passo foi adicionar uma interface CLI às operações CRUD. Isso também foi muito fácil de fazer. Eu uso o crate clap
para analisar os argumentos e, em seguida, uso o crate inquire
para fazer perguntas. O crate clap
é muito fácil de usar. Basta definir os argumentos em uma struct e, em seguida, usar as macros clap para analisar os argumentos. A aparência é mais ou menos assim:
#[derive(Parser, Debug)]
#[command(name="Handler", author="Ishan Joshi", version, about="A Simple CLI to handle my site", long_about = None)]
// A struct Args é usada para analisar os argumentos da linha de comando
struct Args {
#[arg(required=true)]
option: String,
#[arg(required=false)]
hash: Option<String>,
}
É isso aí! É simples assim. Ele vem com tudo, o comando help, o comando version e assim por diante. Você também pode adicionar seus próprios comandos e subcomandos. Eu só uso a macro arg para definir os argumentos. Obtemos os argumentos chamando Args::parse()
. Isso retorna uma estrutura com os argumentos analisados.
Porém, não gosto de impor argumentos ao usuário. Gosto de fazer perguntas. Por isso, uso o crate inquire
para fazer perguntas. O inquire é um crate muito útil que implementa com perfeição a maioria das operações padrão de entrada e saída. Assim, você pode pedir confirmação, pedir uma senha, pedir uma lista de opções e assim por diante. Você pode até mesmo fazer com que ele abra um editor de texto para que você escreva sua resposta. É um crate muito útil. Então, eu o usei para escrever uma função personalizada que recebe uma instância de uma struct e, em seguida, faz perguntas com base na struct. A struct quips tem a seguinte aparência:
pub fn ask_page(page: &Page)->Page{
println!("{}", format!("Editing {}", page.name).as_str());
let name = inquire::Text::new("Name").with_default(page.name.as_str()).prompt().unwrap();
let content = inquire::Editor::new("Content").with_file_extension(".md").with_editor_command(std::ffi::OsStr::new("vim")).with_predefined_text(page.content.as_str()).prompt().unwrap();
let author = inquire::Text::new("Author").with_default(page.author.as_str()).prompt().unwrap();
let date = inquire::Text::new("Date").with_default(page.date.as_str()).prompt().unwrap();
let page = Page{
_id: page._id.clone(),
hash: page.hash.clone(),
name,
content,
author,
date,
};
page
}
Isso trará uma saída assim:
O que é muito legal. Quero dizer, é uma ferramenta CLI, mas parece um GUI. Eu adoro isso.
Também tive que implementar uma função search. Usei o crate inquire
para isso também. Simplesmente, usei a struct Select
para solicitar uma lista de opções e passei na lista de páginas. A struct Select
do inquire
é muito poderosa. Ela solicita automaticamente que o usuário comece a digitar e, em seguida, filtra a lista com base na entrada. Então, eu simplesmente passei na lista de páginas e obtive uma lista de páginas que eu poderia selecionar. A busca foi resolvida.
Compilando
Em seguida, tudo o que eu precisei fazer foi integrar tudo isso. Eu só precisei chamar as funções na ordem correta. Foi bem fácil e, depois de muito debugging, finalmente consegui fazer com que funcionasse. Então, no momento da verdade, executei o cargo build --release
e obtive um único executável binário. Ele tinha apenas 914kb de tamanho! Agora tenho um CMS totalmente funcional que tem apenas 914kb.
A melhor parte? Não preciso carregar ou clonar nada, se quiser usá-lo em outra máquina. Só preciso fazer o download do executável, que será hospedado em meu site, e então poderei usá-lo para gerenciar meu conteúdo.
Conclusão
Então, o que você pode tirar disso? Bem, você pode tirar o fato de que é possível escrever um CMS em Rust. O Rust é um instrumento fantástico para escrever ferramentas CLI. É rápido, fácil de usar e fácil de enviar. Depois de tudo isso, acabei escrevendo apenas cerca de 600 linhas de código. Isso é simplesmente incrível.
Essa também foi uma boa experiência de aprendizado para mim. Aprendi muito sobre Rust, tokio e futures. No geral, foi muito legal. Então, para concluir, você deveria escrever um CMS em Rust? Bem, se você quiser, vá em frente. No entanto, às vezes, a melhor opção é usar um bom framework da Web. Quero dizer, você não precisa escrever um CMS para tudo. No meu caso, eu tinha um caso de uso muito pequeno e queria escrever uma ferramenta CLI. Por isso, escrevi um CMS em Rust. No entanto, provavelmente sim, recomendo que você escreva um CMS CLI, pois com isso, você aprenderá muito sobre a linguagem e suas bibliotecas, além de ser divertido.
De qualquer forma, espero que você tenha gostado desta postagem. Sei que ela foi um pouco mais técnica do que minhas outras postagens, mas espero que tenha gostado. Se quiser dar uma olhada no código, você pode consultá-lo em newtoallofthis123/noob_handler. Você pode até cloná-lo, mas não poderá realmente usá-lo, pois ele está configurado para funcionar com meu banco de dados. Obrigado pela leitura e nos vemos na próxima. Tchau!
Esse artigo foi escrito por Ishan Joshi e traduzido por Fátima Lima. O original pode ser lido aqui.
Latest comments (0)