Desenvolvimento Rust com Claude Code: Cargo, ownership, testes e CLI
Guia prático de Rust com Claude Code: Cargo, ownership, testes, fmt, clippy, erros e um CLI pequeno copiável.
Comece com uma tarefa Rust pequena e verificável
Rust parece difícil no início por causa de ownership, borrowing, lifetimes e tratamento de erro com Result. Uma tradução prática é: ownership define quem possui o dado, borrowing define quem pega emprestado para ler ou modificar, e lifetime define por quanto tempo essa referência é válida. Claude Code ajuda quando trabalha junto com o compilador: explica mensagens, edita arquivos pequenos e executa os comandos de validação.
Neste guia vamos criar um CLI chamado tasknote. Ele lê um tasks.txt com linhas [ ] task e [x] task, mostra um resumo e também lista apenas tarefas abertas. As referências oficiais são Rust Book sobre ownership, Cargo Book para criar projeto, cargo test, rustfmt, Clippy e Claude Code overview.
Se Claude Code ainda é novo para você, leia o guia inicial. Em times, combine com boas práticas de CLAUDE.md e com o guia de permissões.
flowchart LR
Prompt["Objetivo e limites"]
Cargo["Projeto Cargo pequeno"]
Compiler["Ler erros de ownership"]
Tests["Fixar comportamento"]
Quality["fmt e clippy"]
Refactor["Refatorar com diff pequeno"]
Prompt --> Cargo --> Compiler --> Tests --> Quality --> Refactor
Criar o projeto com Cargo
Cargo é a ferramenta padrão para criar pacotes Rust, compilar, executar, testar e gerenciar dependências. Antes de pedir código ao Claude Code, defina quais comandos provam que a tarefa terminou.
cargo new tasknote --bin
cd tasknote
cargo run
O projeto gerado inclui Cargo.toml e src/main.rs. Projetos atuais podem usar edition = "2024", então peça para Claude Code ler o manifesto real. Neste exemplo usamos só clap para argumentos de linha de comando e anyhow para contexto de erro.
[package]
name = "tasknote"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
O primeiro prompt deve pedir desenho, não implementação completa.
Neste projeto Cargo, quero criar um CLI `tasknote`.
Ele lê linhas `[ ] task` e `[x] task` em `tasks.txt`.
Primeiro retorne apenas o desenho de `Cargo.toml`, `src/lib.rs` e `src/main.rs`.
Ainda não edite arquivos. Explique as escolhas de ownership e tratamento de erro.
Essa pausa evita código difícil de manter. Em Rust, a pergunta “quem possui este dado?” precisa vir cedo.
Como perguntar sobre ownership e borrowing
No CLI, o conteúdo do arquivo vira String. parse_tasks recebe &str porque só lê o texto. O resultado é Vec<Task>, com cada tarefa possuindo seu título. summarize recebe &[Task] porque apenas percorre a lista.
Explique o ownership de `parse_tasks(input: &str) -> Vec<Task>` e `summarize(tasks: &[Task]) -> String`.
Use somente este CLI como exemplo.
Explique `String`, `&str`, `Vec<Task>` e `&[Task]` para iniciantes.
Não adicione `clone()` desnecessário.
Um erro comum é pedir para “corrigir o borrow checker” e aceitar vários clone(). Clonar pode ser correto, mas precisa de motivo. Se a função só lê dados, borrowing costuma ser melhor.
Exemplo de CLI copiável
Coloque a lógica em src/lib.rs, para testar sem iniciar o CLI.
// src/lib.rs
use anyhow::{Context, Result};
use std::{fs, path::Path};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Task {
pub title: String,
pub done: bool,
}
pub fn parse_tasks(input: &str) -> Vec<Task> {
input.lines().filter_map(parse_task_line).collect()
}
fn parse_task_line(line: &str) -> Option<Task> {
let trimmed = line.trim();
if let Some(title) = trimmed
.strip_prefix("[x] ")
.or_else(|| trimmed.strip_prefix("[X] "))
{
return Some(Task {
title: title.trim().to_string(),
done: true,
});
}
if let Some(title) = trimmed.strip_prefix("[ ] ") {
return Some(Task {
title: title.trim().to_string(),
done: false,
});
}
None
}
pub fn summarize(tasks: &[Task]) -> String {
let total = tasks.len();
let done = tasks.iter().filter(|task| task.done).count();
let open = total.saturating_sub(done);
format!("{total} tasks: {done} done, {open} open")
}
pub fn read_tasks(path: impl AsRef<Path>) -> Result<Vec<Task>> {
let path = path.as_ref();
let content = fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
Ok(parse_tasks(&content))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_markdown_style_tasks() {
let tasks = parse_tasks("[ ] write parser\n[x] add tests\n[X] run clippy\n");
assert_eq!(
tasks,
vec![
Task {
title: "write parser".to_string(),
done: false,
},
Task {
title: "add tests".to_string(),
done: true,
},
Task {
title: "run clippy".to_string(),
done: true,
},
]
);
}
#[test]
fn ignores_unrecognized_lines() {
let tasks = parse_tasks("# Sprint notes\n- plain bullet\n[ ] keep this\n");
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].title, "keep this");
}
#[test]
fn summarizes_counts() {
let tasks = parse_tasks("[ ] one\n[x] two\n[ ] three\n");
assert_eq!(summarize(&tasks), "3 tasks: 1 done, 2 open");
}
}
O main.rs fica pequeno.
// src/main.rs
use anyhow::Result;
use clap::Parser;
use std::path::PathBuf;
use tasknote::{read_tasks, summarize};
#[derive(Parser, Debug)]
#[command(name = "tasknote", about = "Summarize simple task files")]
struct Cli {
#[arg(short, long, default_value = "tasks.txt")]
file: PathBuf,
#[arg(long)]
only_open: bool,
}
fn main() -> Result<()> {
let args = Cli::parse();
let tasks = read_tasks(&args.file)?;
if args.only_open {
for task in tasks.iter().filter(|task| !task.done) {
println!("- {}", task.title);
}
} else {
println!("{}", summarize(&tasks));
}
Ok(())
}
Exemplo de tasks.txt:
[ ] write parser
[x] add unit tests
[ ] run clippy
Comandos:
cargo fmt
cargo test
cargo clippy --all-targets -- -D warnings
cargo run -- --file tasks.txt
cargo run -- --file tasks.txt --only-open
Como main retorna Result<()>, um arquivo ausente pode mostrar contexto útil em vez de um unwrap() sem explicação. Para CLI, entrada ausente é erro recuperável.
Colocar testes, fmt e Clippy na tarefa
Peça a verificação no mesmo prompt da implementação.
Implemente `src/lib.rs` e `src/main.rs`.
Depois rode `cargo fmt`, `cargo test` e `cargo clippy --all-targets -- -D warnings`.
Se falhar, resuma o erro e corrija com o menor diff razoável.
Não use `unwrap()` fora dos testes.
Claude Code é descrito oficialmente como uma ferramenta agentic que lê código, edita arquivos e executa comandos. Mesmo assim, revise o git diff, confirme o escopo e rode comandos críticos por conta própria quando o risco for alto.
Casos de uso práticos
Primeiro: adicionar uma opção a um CLI existente. Para --json, mantenha summarize estável e crie função separada para formatação.
Segundo: aprender com erros de ownership. Cole o erro completo e peça para explicar quem possui o valor, quem empresta e qual referência vive demais.
Terceiro: correção com testes primeiro. Escreva testes para linha vazia, [X] e linha inválida antes de mudar o parser.
Quarto: refatoração segura. Quando main.rs crescer, separe parsing, I/O e apresentação sem mudar assinaturas públicas.
Armadilhas comuns
Não resolva todos os erros de borrowing com clone(). Pergunte se borrowing basta.
Não deixe unwrap() em caminhos de produção. Arquivos, configuração e entrada de usuário falham.
Não refatore sem testes. Claude Code pode gerar grandes diffs rápido, mas revisão fica difícil.
Não formate um workspace inteiro se a tarefa toca apenas um crate.
Prompt seguro de refatoração
Refatore com segurança o parser de `tasknote`.
Condições:
- Preservar o significado de `[ ] task` e `[x] task`
- Não mudar as assinaturas públicas de `parse_tasks`, `summarize` e `read_tasks`
- Tocar apenas `src/lib.rs` e seus testes
- Não adicionar `clone()` desnecessário
- Primeiro mostrar um plano de 3 linhas e esperar aprovação
Depois:
- `cargo fmt`
- `cargo test`
- `cargo clippy --all-targets -- -D warnings`
- Resumir o diff e explicar decisões de ownership
Esse prompt cria limites, checks e relatório. Rust combina bem com esse método porque compilador, testes e Clippy dão feedback rápido.
CTA e próximos passos
Para praticar, adicione JSON, CSV, varredura de diretórios ou formato com serde. Para times, coloque no CLAUDE.md a edition, comandos obrigatórios, política de unwrap(), crates permitidos e checklist.
ClaudeCodeLab oferece prompts, templates, guias e treinamento para Claude Code. Comece pelo cheatsheet gratuito, veja produtos e templates ou use treinamento e consultoria para adaptar ao seu repositório. Leia também TDD com Claude Code e o checklist de revisão.
Nota de teste: o fluxo mais estável foi manter a lógica em src/lib.rs, fixar comportamento com cargo test e pedir a explicação de ownership antes da edição. Isso reduz clone() desnecessário e deixa o diff menor.
Artigos relacionados
Desenvolvimento Go com Claude Code: go.mod, testes, concorrencia e race detector
Guia pratico Claude Code para Go: mapa do repo, go.mod, go work, API/CLI, testes, context, concorrencia, benchmarks.
Desenvolver ferramentas CLI com Claude Code: Node.js, TypeScript, testes e releases seguros
Crie uma CLI Node.js/TypeScript com Claude Code: argumentos, config, stdin/stdout, exit codes, testes, CI e segredos.
Desenvolvimento Tauri com Claude Code: Vite, comandos Rust e permissoes
Guia pratico de Tauri v2 com Claude Code: Vite, Rust commands, invoke, permissoes de arquivos, testes e review.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.