Quer sair da interface do usuário e criar algo legal? Aqui está um tutorial sobre como criar sua própria CLI em Rust.
[fig. Uma foto do meu terminal]
O mundo do terminal é fascinante, e eu sempre adoro explorá-lo. Longe da pequena e antiquada experiência da web, que está se tornando bastante 'rotineira' atualmente, você pode construir um pequeno canto onde está no total controle. Portanto, aqui está um pequeno tutorial sobre como construir sua própria CLI em Rust. Vou te mostrar como tornar a saída mais bonita, conexões e muito mais. Então, vamos começar.
Este tutorial de forma alguma é um guia completo. Isso é apenas para mostrar a maneira básica pela qual você pode abordar a criação de uma CLI e como começar a fazê-lo. Este artigo também presume que você tem um conhecimento bom o suficiente da linguagem Rust. Se você não tiver, recomendo que consulte o livro oficial doe Rust. É um recurso muito bom para aprender Rust. Você pode encontrá-lo aqui: https://doc.rust-lang.org/book/
O que estamos construindo?
Então, o que vamos construir é na verdade bastante simples e você pode realmente achar isso. Vamos construir um 'Armazenamento Chave-Valor'. Portanto, o conceito é bastante simples. Temos 5 comandos:
1.Adicionar uma Chave-Valor [C]
2.Ler um Valor pela Chave [R]
3.Atualizar um Valor pela Chave [U]
4.Excluir um Valor pela Chave [D]
5.Pesquisar uma Chave/Valor [S]
O clássico aplicativo CRUDS. É assim que vamos fazer isso.
Vamos de Rust
Vamos usar o Rust. O Rust é uma linguagem muito simples de usar, segura em relação à memória e aos tipos, e é excelente para construir CLIs incríveis e confiáveis. Na verdade, rapidamente se tornou a ferramenta número um para a construção de CLIs. Vou entrar em mais detalhes sobre por que as CLIs do Rust são boas em um futuro post no blog, então fique ligado para isso. Com isso, vamos configurar nosso projeto. Instale o Rust em sua máquina se ainda não o fez. Você pode fazer isso visitando o site da Linguagem de Programação Rust e simplesmente baixando o Rust para sua plataforma. Depois que isso estiver feito, vá até o seu terminal. O terminal é uma excelente maneira de interagir com seu computador. Se você quiser saber o básico, há muitos recursos no YouTube. Abaixo está um que eu recomendo do freeCodeCamp. Com isso, acesse um novo diretório cd
e execute:
cargo init . --bin --name "nkv"
Estamos usando --bin
para indicar ao Cargo que usaremos isso como um binário, em vez de uma biblioteca (crate), apenas para deixar claro. Agora, abra isso em seu editor de texto favorito. Eu pessoalmente uso o VSCode (eu sei, chocante), mas o Rust possui um ecossistema ótimo para Neovim e Emacs também. Agora, crie todos os arquivos mostrados abaixo:
Não se preocupe, você não precisa ter o SQLITE3 instalado para que isso funcione. Vamos usar diretamente uma crate do Rust. Vamos dar uma rápida olhada na estrutura de arquivos:
Teremos quatro arquivos na pasta src
, incluindo o main.rs
:
- db.rs: Lida com conexões de banco de dados e a lógica das operações CRUD.
- handler.rs: Age essencialmente como um middleware.
- utils.rs: Contém algumas funções utilitárias legais, como cópia, geração de hash, etc.
- Um arquivo
.env
para armazenar nosso endereço de banco de dados. - main.db, que é nosso banco de dados de desenvolvimento.
Agora que você criou esses arquivos, é hora de instalar alguns crates. Crates são bibliotecas independentes do Rust que fornecem recursos adicionais. Eles são construídos e mantidos pela comunidade e são muito úteis. É muito fácil instalá-los em seu projeto Rust. Basta ir até o cargo.toml
e, em [dependencies]
, cole o nome da dependência. Abaixo, listarei o que estamos usando, no entanto, se você quiser encontrar mais crates do Rust, recomendo que consulte a Lista do GitHub vinculada abaixo.
https://github.com/rust-unofficial/awesome-rust
Isso vai para o nosso cargo.toml:
[package]
name = "nkv"
version = "0.2.0"
edition = "2021"
[dependencies]
clap = {version = "4.3.23", features = ["derive"]}
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
serde = {version = "*", features=["derive"]}
bunt = "*"
inquire = "*"
tokio = {version="*", features=["full"]}
chrono = "*"
clipboard = { version = "0.5.0"}
rand = "*"
dotenv = {version="*"}
tabled ={ version="*"}
human-panic = "*"
Vamos usar algumas dependências para criar uma CLI bonita. A dependência clap
é um analisador de argumentos CLI muito simples de usar que usa uma struct predefinida para gerenciamento. sqlx
é um simples vinculador SQL, não ORM, para Rust, que torna muito fácil executar consultas SQL em nosso banco de dados.
serde
é uma popular biblioteca JSON, usamos chrono
para gerenciar coisas relacionadas ao tempo. clipboard
é uma abstração intuitiva da área de transferência para o Rust. rand
é para geração aleatória, dotenv
para ler do arquivo .env, tabled
para estilizar uma struct em uma tabela e tokio
para tornar nossa função assíncrona, para que a execução seja bloqueada até que nosso banco de dados seja consultado. bunt
e inquire
são bibliotecas de formatação e solicitação de CLI semelhantes à biblioteca rich
em Python, Uau. Então, terminamos com as bibliotecas. Certifique-se também de incluir os mesmos recursos que eu incluí, pois usaremos todos eles.
Vamos começar
Agora, finalmente podemos começar a codificação real. Abra o arquivo [main.rs](http://main.rs)
e configure o clap::Parser
para uma struct agradável.
use clap::Parser;
use dotenv::dotenv;
use human_panic::setup_panic;
#[derive(Parser, Debug)]
#[command(name="NoobKey", author="Ishan Joshi", version, about="A Simple Key Value Store", long_about = None)]
//? A estrutura Args é usada para analisar os argumentos da linha de comando
struct Args {
#[arg(required=false)]
cmd: Option<String>,
#[arg(short, long)]
custom: Option<String>,
#[arg(short, long)]
docs: bool,
}
mod db;
mod utils;
mod handler;
O cmd
é um argumento opcional e solicitaremos ao usuário que o insira caso não seja fornecido. Queremos esta CLI para uso pessoal, portanto, não é necessário se preocupar muito. Também adicionamos todos os arquivos ao arquivo principal como mods. Agora, na função principal, temos todas as configurações predefinidas dotenv().ok()
; setup_panic!()
que cuidarão do ambiente e dos erros na produção. Desta forma, vamos analisar os argumentos e verificar se o usuário inseriu o comando. Se ele não o fez, vamos pedir a ele que o faça.
let args = Args::parse();
let cmd:String;
if args.cmd.is_some(){
cmd = args.cmd.unwrap();
}
else{
cmd = inquire::Text::new("Enter Command: ").with_help_message("Enter a valid command").with_autocomplete(
&utils::suggester,
).prompt().unwrap();
}
A utils::suggester
é uma função simples de filtro e mapeamento que a inquire
utiliza como entrada, aqui está a função. Você pode abrir utils e colar isso lá.
pub fn suggester(val: &str) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
let suggestions = [
"get",
"set",
"delete",
"list",
"search",
"help",
"exit",
];
let val_lower = val.to_lowercase();
Ok(suggestions
.iter()
.filter(|s| s.to_lowercase().contains(&val_lower))
.map(|s| String::from(*s))
.collect())
}
Basicamente, adicionamos apenas uma função simples de filtro e mapeamento em Rust.
O Banco de Dados
Vamos configurar a conexão com o banco de dados agora. Primeiro, abra db.rs
e importe os módulos necessários e defina uma struct
de como nossa consulta ao banco de dados seria.
use sqlx::Connection;
use serde::{Deserialize, Serialize};
use sqlx::Row;
#[derive(sqlx::FromRow, Serialize, Deserialize)]
pub struct Entry {
pub id: i32,
pub key: String,
pub value: String,
pub hash: String,
pub created_at: String,
}
Usamos serde
para serializar e desserializar dados, os quais são derivados do sqlx
. Nossa entrada terá uma id simples, chave, valor, hash e carimbo de data/hora (timestamp). Uma estrutura simples e fácil de trabalhar. Agora, vamos obter a conexão com o banco de dados usando sqlx
e retornar uma conexão para a tabela que estou chamando de entries
.
async fn get_db() -> sqlx::Result<sqlx::SqliteConnection> {
//usado para conectar-se ao DATABASE_URL
// Pode ser qualquer string de conexão SQLite válida
sqlx::SqliteConnection::connect(format!("sqlite:{}", std::env::var("KEY_STORE").unwrap()).as_str()).await
}
pub async fn connect() -> sqlx::SqliteConnection {
//criar tabela para fazer se não existir
let mut conn = get_db().await.expect("Error connecting to db");
let _ = sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT NOT NULL,
value TEXT NOT NULL,
hash TEXT NOT NULL,
created_at TEXT NOT NULL
)
"#,
).execute(&mut conn).await.unwrap();
conn
}
Como você pode ver, na função connect, usamos apenas uma consulta SQL. Nenhuma ORM é necessária.
Agora, podemos usar a função connect()
para retornar uma conexão do banco de dados, sem a necessidade de reconectar toda vez.
Assim, definimos uma função add_to_db
, a parte de criação do CRUD.
pub async fn add(key: String, value: String, hash: String){
let mut conn = connect().await;
let _ = sqlx::query(
r#"
INSERT INTO entries (key, value, hash, created_at)
VALUES (?, ?, ?, ?)
"#,
)
.bind(key)
.bind(value)
.bind(hash.clone())
.bind(chrono::Local::now().to_rfc3339())
.execute(&mut conn)
.await.expect("Error adding entry");
}
Como você pode ver, usamos SQL simples. O sqlx
nos permite vincular o ?
a um valor, que é exatamente o que fizemos. Também usamos o horário chrono::Local
no formato ISO como carimbo de data/hora.
Escrevemos de forma semelhante o restante do RUD:
//obter do db
pub async fn get(key: String) -> sqlx::Result<Entry> {
let mut conn = connect().await;
let row = sqlx::query("SELECT * FROM entries WHERE key = ?")
.bind(key)
.fetch_one(&mut conn)
.await?;
Ok(Entry {
id: row.get(0),
key: row.get(1),
value: row.get(2),
hash: row.get(3),
created_at: row.get(4),
})
}
//delete do db
pub async fn delete(key: String) -> sqlx::Result<()> {
let mut conn = connect().await;
sqlx::query("DELETE FROM entries WHERE key = ?")
.bind(key)
.execute(&mut conn)
.await?;
Ok(())
}
//listar todas as entradas
pub async fn list() -> sqlx::Result<Vec<Entry>> {
let mut conn = connect().await;
let mut entries = vec![];
let rows = sqlx::query("SELECT * FROM entries")
.fetch_all(&mut conn)
.await?;
for row in rows {
entries.push(Entry {
id: row.get(0),
key: row.get(1),
value: row.get(2),
hash: row.get(3),
created_at: row.get(4),
});
}
Ok(entries)
}
//listar todas as chaves
pub async fn list_keys() -> sqlx::Result<Vec<String>> {
let mut conn = connect().await;
let mut keys = vec![];
let rows = sqlx::query("SELECT key FROM entries")
.fetch_all(&mut conn)
.await?;
for row in rows {
keys.push(row.get(0));
}
Ok(keys)
}
O Handler
Agora, definimos um arquivo semelhante a um middleware para lidar com todas as operações e exibi-las adequadamente ao usuário. Por exemplo, podemos pedir ao usuário para adicionar algo assim.
pub async fn add(){
bunt::println!("Executing add command...");
let key = inquire::Text::new("Enter Key: ").with_help_message("Enter any identifier").prompt().unwrap();
let value = inquire::Text::new("Enter Value: ").with_help_message("Enter any value").prompt().unwrap();
let hash = super::utils::random_hash();
super::db::add(key.clone(), value.clone(), hash).await;
bunt::println!("Added entry: {$green}{}{/$}", key);
bunt::println!("Value: {$yellow}{}{/$}", value);
}
Agora que definimos a função add
, podemos simplesmente usar uma declaração match no arquivo principal para chamar o handler.
match cmd.as_str() {
"set" => handler::add().await,
"list" => handler::list().await,
"delete" => handler::delete().await,
"get" => handler::get().await,
"search" => handler::search().await,
"exit" => {
bunt::println!("{$red}Exiting...{/$}");
std::process::exit(0);
}
"help" => todo!("Help command not implemented"),
_ => todo!("Command not found")
}
Assim como defini as funções, você pode definir as suas próprias também.
Agora, é aqui que eu deixo você.
Conclusão
O principal objetivo deste artigo era ajudá-lo a dar os primeiros passos na escrita da sua própria CLI usando Rust, e todo esse propósito seria perdido se eu impusesse minhas próprias ideias a você. Agora, você está livre para criar sua própria CLI. Formate-a como quiser. Talvez, usando um pouco de magia do Rust, você possa criar uma CLI muito bonita. Você tem a liberdade para usar o NoobKey
como referência ou até mesmo construir em cima dele. É um código-base bastante simples e fácil de usar. Este é o repositório GitHub dele:
https://github.com/newtoallofthis123/noob_key
Dito isso, obrigado por ler. A verdadeira razão pela qual escrevi este artigo foi testar como os blocos de código ficam no meu site 😅. Você pode conferir isso aqui: https://noobscience.rocks/blog/tutorials/rust-cli. Obrigado por ler, espero que tenha um ótimo dia.
Esse artigo é uma tradução feita por @bananlabs. Você pode encontrar o artigo original aqui
Latest comments (0)