WEB3DEV

Cover image for Escrevendo sua própria CLI em Rust
Banana Labs
Banana Labs

Posted on

Escrevendo sua própria CLI em Rust

Quer sair da interface do usuário e criar algo legal? Aqui está um tutorial sobre como criar sua própria CLI em Rust.

capa

[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"
Enter fullscreen mode Exit fullscreen mode

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:

arquivos

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 = "*"
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();

    }
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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");
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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")
    }

Enter fullscreen mode Exit fullscreen mode

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)