Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Refatoração para melhorar a modularidade e o tratamento de erros

Para melhorar nosso programa, vamos corrigir quatro problemas relacionados à estrutura do código e à forma como ele lida com possíveis erros. Primeiro, a função main agora executa duas tarefas: analisar argumentos e ler arquivos. À medida que o programa crescer, o número de tarefas diferentes pelas quais main é responsável tende a aumentar. Quando uma função acumula responsabilidades, ela se torna mais difícil de entender, mais difícil de testar e mais difícil de modificar sem quebrar alguma parte. O ideal é separar as funcionalidades para que cada função fique responsável por uma única tarefa.

Esse problema também se conecta a um segundo ponto: embora query e file_path sejam variáveis de configuração do programa, variáveis como contents são usadas na lógica principal. Quanto maior main ficar, mais variáveis precisaremos colocar em escopo; quanto mais variáveis houver em escopo, mais difícil será acompanhar o propósito de cada uma. O ideal é agrupar as variáveis de configuração em uma struct para deixar essa intenção clara.

O terceiro problema é que usamos expect para imprimir uma mensagem de erro quando a leitura do arquivo falha, mas a mensagem gerada apenas diz Should have been able to read the file. Ler um arquivo pode falhar por várias razões: o arquivo pode não existir ou talvez não tenhamos permissão para abri-lo. No estado atual, independentemente do motivo, imprimiríamos a mesma mensagem para tudo, o que não dá nenhuma informação útil ao usuário.

Por fim, também usamos expect para lidar com outro erro: se o usuário rodar o programa sem argumentos suficientes, receberá um erro index out of bounds gerado pelo próprio Rust, o que não explica claramente o problema. Seria melhor se todo o código de tratamento de erros ficasse reunido em um só lugar, de modo que futuras pessoas mantenedoras soubessem exatamente onde procurar se essa lógica precisasse mudar. Além disso, manter esse tratamento concentrado em um ponto também ajuda a garantir que as mensagens impressas sejam úteis para os usuários finais.

Vamos resolver esses quatro problemas refatorando o projeto.

Separando Responsabilidades em Projetos Binários

O problema organizacional de concentrar muitas tarefas na função main é comum em vários projetos binários. Por isso, muitos programadores Rust consideram útil separar as diferentes responsabilidades de um programa binário quando main começa a crescer. Esse processo costuma seguir estas etapas:

  • Divida o programa em um arquivo main.rs e um arquivo lib.rs, movendo a lógica principal para lib.rs.
  • Enquanto a lógica de análise de linha de comando for pequena, ela pode continuar dentro de main.
  • Quando essa lógica de análise começar a ficar mais complexa, extraia-a de main para outras funções ou tipos.

Depois desse processo, as responsabilidades restantes em main devem se limitar a:

  • Chamar a lógica de análise de linha de comando com os valores dos argumentos
  • Configurar qualquer informação adicional necessária
  • Chamar uma função run em lib.rs
  • Tratar o erro caso run retorne um erro

Esse padrão trata de separação de responsabilidades: main.rs cuida da execução do programa, enquanto lib.rs concentra toda a lógica da tarefa em si. Como não é possível testar diretamente a função main, essa estrutura permite testar toda a lógica do programa ao movê-la para fora dela. O código que permanecer em main ficará pequeno o bastante para termos confiança nele apenas lendo-o. Vamos retrabalhar o programa seguindo esse processo.

Extraindo o Analisador de Argumentos

Vamos extrair a funcionalidade de análise de argumentos para uma função que main chamará. A Listagem 12-5 mostra o novo começo de main, que passa a chamar uma nova função parse_config, a ser definida em src/main.rs.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let (query, file_path) = parse_config(&args);

    // --snip--

    println!("Searching for {query}");
    println!("In file {file_path}");

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

fn parse_config(args: &[String]) -> (&str, &str) {
    let query = &args[1];
    let file_path = &args[2];

    (query, file_path)
}
Listing 12-5: Extraindo de main uma função parse_config

Ainda continuamos coletando os argumentos de linha de comando em um vetor, mas, em vez de atribuir dentro de main o valor no índice 1 à variável query e o valor no índice 2 à variável file_path, passamos o vetor inteiro para parse_config. Essa função passa a conter a lógica que determina qual argumento vai para qual variável e devolve os valores a main. Continuamos criando as variáveis query e file_path em main, mas main deixa de ser responsável por determinar como argumentos e variáveis se correspondem.

Essa mudança pode parecer exagerada para um programa tão pequeno, mas estamos refatorando em passos pequenos e incrementais. Depois de fazer essa alteração, rode o programa novamente para verificar se a análise dos argumentos continua funcionando. É uma boa prática validar o progresso com frequência, porque isso ajuda a localizar a origem de problemas quando eles aparecem.

Agrupando Valores de Configuração

Podemos dar mais um passo pequeno para melhorar parse_config. No momento, estamos retornando uma tupla, mas logo em seguida quebramos essa tupla de volta em partes individuais. Isso é um sinal de que talvez ainda não tenhamos a abstração certa.

Outro indício de que há espaço para melhoria é a própria palavra config em parse_config, que sugere que os dois valores retornados estão relacionados e fazem parte de uma única configuração. Hoje, não estamos transmitindo esse significado na estrutura dos dados, exceto por agrupá-los em uma tupla. Em vez disso, vamos colocar os dois valores em uma struct e dar a cada campo um nome significativo. Isso tornará mais fácil para futuras pessoas mantenedoras entenderem como os diferentes valores se relacionam e qual é o propósito de cada um.

A Listagem 12-6 mostra as melhorias na função parse_config.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    // --snip--

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let file_path = args[2].clone();

    Config { query, file_path }
}
Listing 12-6: Refatorando parse_config para retornar uma instância da struct Config

Adicionamos uma struct chamada Config com campos query e file_path. A assinatura de parse_config agora indica que ela retorna um valor Config. No corpo de parse_config, onde antes retornávamos fatias de string que referenciavam valores String em args, agora definimos Config para conter valores String com ownership próprio. A variável args, em main, é a dona dos valores dos argumentos e apenas permite que parse_config os empreste. Se Config tentasse tomar ownership desses valores diretamente de args, violaríamos as regras de borrowing do Rust.

Há várias formas de lidar com esses dados String; o caminho mais simples, embora um pouco ineficiente, é chamar clone nos valores. Isso fará uma cópia completa dos dados para que a instância de Config tenha ownership deles, o que consome mais tempo e memória do que armazenar uma referência para os dados da string. Ainda assim, clonar também deixa o código muito mais simples, porque não precisamos gerenciar o lifetime das referências; nessa situação, abrir mão de um pouco de desempenho em troca de simplicidade é uma decisão razoável.

As vantagens e desvantagens de usar clone

Há uma tendência entre muitos Rustáceos de evitar usar clone para consertar problemas de ownership devido ao custo em tempo de execução. Em Capítulo 13, você aprenderá como usar recursos mais eficientes métodos neste tipo de situação. Mas, por enquanto, não há problema em copiar alguns strings para continuar progredindo porque você fará apenas essas cópias uma vez e o caminho do arquivo e a string de consulta são muito pequenos. É melhor ter um programa funcional que é um pouco ineficiente do que tentar hiperotimizar o código na sua primeira passagem. À medida que você se torna mais experiente com Rust, será mais fácil começar com a solução mais eficiente, mas por enquanto, é perfeitamente aceitável ligar para clone.

Atualizamos main para armazenar a instância de Config retornada por parse_config em uma variável chamada config. Também ajustamos o restante do código, que antes usava as variáveis query e file_path separadamente, para passar a usar os campos da struct Config.

Agora o código transmite com mais clareza que query e file_path estão relacionados e que seu propósito é configurar a forma como o programa vai funcionar. Qualquer parte do código que use esses valores sabe que deve encontrá-los na instância config, em campos nomeados de acordo com sua finalidade.

Criando um Construtor para Config

Até aqui, extraímos a lógica responsável por analisar os argumentos de linha de comando de main e a colocamos em parse_config. Isso nos ajudou a perceber que os valores query e file_path estavam relacionados e que essa relação deveria ser expressa no código. Depois, adicionamos uma struct Config para representar esse papel compartilhado e para poder devolver os valores usando nomes de campos significativos.

Agora, como o objetivo de parse_config é criar uma instância de Config, podemos transformar parse_config em vez de uma função comum em uma função chamada new, associada à struct Config. Essa mudança torna o código mais idiomático. Criamos instâncias de tipos da biblioteca padrão, como String, chamando String::new. Da mesma forma, ao transformar parse_config em uma função new associada a Config, poderemos criar instâncias de Config chamando Config::new. A Listagem 12-7 mostra as alterações necessárias.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");

    // --snip--
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}
Listing 12-7: Transformando parse_config em Config::new

Atualizamos main para chamar Config::new onde antes chamávamos parse_config. Mudamos o nome de parse_config para new e o movemos para dentro de um bloco impl, o que associa essa função a Config. Tente compilar o código novamente para ter certeza de que tudo continua funcionando.

Corrigindo o Tratamento de Erros

Agora vamos trabalhar para melhorar o tratamento de erros. Lembre-se de que tentar acessar os valores do vetor args nos índices 1 ou 2 fará o programa entrar em pânico se o vetor tiver menos de três itens. Tente executar o programa sem nenhum argumento; o resultado será algo assim:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`

thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

A linha index out of bounds: the len is 1 but the index is 1 é uma mensagem de erro voltada para programadores. Ela não ajuda o usuário final a entender o que deveria fazer. Vamos corrigir isso agora.

Melhorando a Mensagem de Erro

Na Listagem 12-8, adicionamos à função new uma verificação para confirmar se a fatia é longa o bastante antes de acessarmos os índices 1 e 2. Se não for, o programa entra em pânico e exibe uma mensagem de erro melhor.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    // --snip--
    fn new(args: &[String]) -> Config {
        if args.len() < 3 {
            panic!("not enough arguments");
        }
        // --snip--

        let query = args[1].clone();
        let file_path = args[2].clone();

        Config { query, file_path }
    }
}
Listing 12-8: Adicionando uma verificação da quantidade de argumentos

Esse código é parecido com a função Guess::new que escrevemos na Listagem 9-13, em que chamávamos panic! quando o argumento value estava fora do intervalo de valores válidos. Aqui, em vez de verificar um intervalo de valores, verificamos se args tem pelo menos 3 itens e então assumimos que o restante da função pode operar com essa condição satisfeita. Se args tiver menos de três itens, essa condição será verdadeira, e chamaremos a macro panic! para encerrar o programa imediatamente.

Com essas poucas linhas extras em new, vamos executar novamente o programa sem argumentos para ver como o erro aparece agora:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep`

thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Essa saída é melhor: agora temos uma mensagem razoável. Ainda assim, ela também traz informações extras que não queremos mostrar aos usuários. Talvez a técnica que usamos na Listagem 9-13 não seja a melhor aqui: uma chamada a panic! é mais apropriada para um problema de programação do que para um problema de uso, como discutimos no Capítulo 9. Em vez disso, vamos usar a outra técnica apresentada no Capítulo 9: retornar um Result indicando sucesso ou erro.

Retornando um Result em vez de chamar panic!

Em vez de entrar em pânico, podemos retornar um valor Result, que conterá uma instância de Config no caso de sucesso e descreverá o problema no caso de erro. Também vamos mudar o nome da função de new para build, porque muitos programadores esperam que funções chamadas new nunca falhem. Quando Config::build se comunica com main, podemos usar o tipo Result para sinalizar que houve um problema. Depois, podemos alterar main para converter uma variante Err em uma mensagem mais prática para usuários, sem o texto extra sobre thread 'main' e RUST_BACKTRACE que uma chamada a panic! costuma produzir.

A Listagem 12-9 mostra as mudanças necessárias no tipo de retorno da função, que agora se chama Config::build, e também no corpo dela para que passe a retornar um Result. Observe que isso ainda não compilará até que também atualizemos main, o que faremos na próxima listagem.

Filename: src/main.rs
use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::new(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}
Listing 12-9: Retornando um Result de Config::build

Nossa função build retorna um Result com uma instância Config no caso de sucesso e uma string literal no caso de erro. Nossos valores de erro serão sempre literais de string, que têm lifetime 'static.

Fizemos duas alterações no corpo da função: em vez de chamar panic! quando o usuário não passa argumentos suficientes, agora retornamos um valor Err; além disso, envolvemos o valor de retorno Config em Ok. Essas mudanças fazem a função obedecer à nova assinatura de tipo.

Retornar um valor Err de Config::build permite que main trate o Result devolvido por build e encerre o processo de maneira mais limpa em caso de erro.

Chamando Config::build e tratando erros

Para lidar com o caso de erro e imprimir uma mensagem amigável, precisamos atualizar main para tratar o Result retornado por Config::build, como mostra a Listagem 12-10. Também vamos assumir explicitamente a responsabilidade de encerrar a ferramenta de linha de comando com um código de erro diferente de zero, em vez de deixar isso a cargo de panic!. Um status de saída diferente de zero é a convenção usada para sinalizar ao processo que invocou nosso programa que ele terminou em estado de erro.

Filename: src/main.rs
use std::env;
use std::fs;
use std::process;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    // --snip--

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}
Listing 12-10: Encerrando com um código de erro se a construção de Config falhar

Nesta listagem, usamos um método que ainda não explicamos em detalhes: unwrap_or_else, definido em Result<T, E> na biblioteca padrão. Usar unwrap_or_else nos permite definir um tratamento de erro personalizado sem recorrer a panic!. Se Result for um valor Ok, o comportamento desse método é parecido com unwrap: ele retorna o valor interno armazenado em Ok. No entanto, se o valor for Err, esse método chama o código do closure que definimos e passamos como argumento para unwrap_or_else. Abordaremos closures com mais detalhes no Capítulo 13. Por enquanto, basta saber que unwrap_or_else passará ao closure, no argumento err entre barras verticais, o valor interno de Err, que neste caso é a string estática "not enough arguments" adicionada na Listagem 12-9. O código do closure pode então usar o valor err quando for executado.

Adicionamos uma nova linha use para trazer process da biblioteca padrão para o escopo. O código dentro do closure executado em caso de erro tem apenas duas linhas: imprimimos o valor err e depois chamamos process::exit. A função process::exit encerra o programa imediatamente e devolve o número passado como código de status de saída. Isso é semelhante ao tratamento com panic! que usamos na Listagem 12-8, mas agora não recebemos toda aquela saída extra. Vamos testar:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments

Ótimo! Esta saída é muito mais amigável para nossos usuários.

Extraindo Lógica de main

Agora que terminamos de refatorar a análise de configuração, vamos voltar para a lógica do programa. Como afirmamos em “Separando Preocupações em Binário Projetos”, iremos extraia uma função chamada run que conterá toda a lógica atualmente no main função que não está envolvida na configuração ou manipulação erros. Quando terminarmos, a função main será concisa e fácil de verificar por inspeção, e seremos capazes de escrever testes para todas as outras lógicas.

A Listagem 12-11 mostra a pequena melhoria incremental da extração de um run função.

Filename: src/main.rs
use std::env;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) {
    let contents = fs::read_to_string(config.file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

// --snip--

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}
Listing 12-11: Extraindo uma função run com o restante da lógica do programa

A função run agora contém toda a lógica restante de main, começando da leitura do arquivo. A função run toma a instância Config como um argumento.

Retornando erros de run

Com a lógica restante do programa separada na função run, podemos melhorar o tratamento de erros, como fizemos com Config::build na Listagem 12-9. Em vez de permitir que o programa entre em pânico chamando expect, o run função retornará Result<T, E> quando algo der errado. Isso vai deixar consolidaremos ainda mais a lógica em torno do tratamento de erros em main em um maneira amigável. A Listagem 12-12 mostra as mudanças que precisamos fazer no assinatura e corpo de run.

Filename: src/main.rs
use std::env;
use std::fs;
use std::process;
use std::error::Error;

// --snip--


fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    run(config);
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}
Listing 12-12: Fazendo a função run retornar Result

Fizemos três mudanças significativas aqui. Primeiro, alteramos o tipo de retorno de a função run para Result<(), Box<dyn Error>>. Esta função anteriormente retornou o tipo de unidade, (), e mantemos isso como o valor retornado no Ok caso.

Para o tipo de erro, usamos o objeto trait Box<dyn Error> (e trouxemos std::error::Error no escopo com uma instrução use no topo). Nós vamos cobrir objetos trait no Capítulo 18. Por enquanto, apenas saiba que Box<dyn Error> significa que a função retornará um tipo que implementa o Error trait, mas não precisamos especificar que tipo específico o retorno valor será. Isso nos dá flexibilidade para retornar valores de erro que podem ser de diferentes tipos em diferentes casos de erro. A palavra-chave dyn é a abreviação de dinâmico.

Segundo, removemos a chamada para expect em favor da operadora ?, pois falado no Capítulo 9. Em vez de panic! em caso de erro, ? retornará o valor do erro da função atual para o chamador lidar.

Terceiro, a função run agora retorna um valor Ok no caso de sucesso. Declaramos o tipo de sucesso da função run como () na assinatura, o que significa que precisamos agrupar o valor do tipo de unidade no valor Ok. Esse A sintaxe Ok(()) pode parecer um pouco estranha no início. Mas usar () assim é a maneira idiomática de indicar que estamos ligando para run por seus efeitos colaterais apenas; ele não retorna um valor que precisamos.

Quando você executa este código, ele será compilado, mas exibirá um aviso:

$ cargo run -- the poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
  --> src/main.rs:19:5
   |
19 |     run(config);
   |     ^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled
   = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
   |
19 |     let _ = run(config);
   |     +++++++

warning: `minigrep` (bin "minigrep") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
     Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Rust nos diz que nosso código ignorou o valor Result e o valor Result pode indicar que ocorreu um erro. Mas não estamos verificando se não houve um erro, e o compilador nos lembra que provavelmente pretendíamos tem algum código de tratamento de erros aqui! Vamos corrigir esse problema agora.

Tratamento de erros retornados de run em main

Verificaremos se há erros e lidaremos com eles usando uma técnica semelhante à que usamos com Config::build na Listagem 12-10, mas com uma pequena diferença:

Nome do arquivo: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

fn main() {
    // --snip--

    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    println!("Searching for {}", config.query);
    println!("In file {}", config.file_path);

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    println!("With text:\n{contents}");

    Ok(())
}

struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

Usamos if let em vez de unwrap_or_else para verificar se run retorna um Err e chamar process::exit(1) se isso acontecer. A função run não retorna um valor que queremos unwrap da mesma forma que Config::build retorna a instância Config. Porque run retorna () em caso de sucesso, nos preocupamos apenas em detectar um erro, então não precisamos unwrap_or_else para retornar o valor desembrulhado, que seria apenas ().

Os corpos das funções if let e unwrap_or_else são os mesmos em ambos os casos: Imprimimos o erro e saímos.

Dividindo o código em uma crate de biblioteca

Nosso projeto minigrep está parecendo bom até agora! Agora vamos dividir o arquivo src/main.rs e coloque algum código no arquivo src/lib.rs. Dessa forma, nós pode testar o código e ter um arquivo src/main.rs com menos responsabilidades.

Vamos definir o código responsável pela pesquisa de texto em src/lib.rs em vez do que em src/main.rs, o que nos permitirá (ou qualquer outra pessoa usando nosso minigrep biblioteca) chama a função de pesquisa em mais contextos do que o nosso minigrep binário.

Primeiro, vamos definir a assinatura da função search em src/lib.rs conforme mostrado em Listagem 12-13, com um corpo que chama a macro unimplemented!. Nós vamos explicar a assinatura com mais detalhes quando preenchermos a implementação.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    unimplemented!();
}
Listing 12-13: Definindo a função search em src/lib.rs

Usamos a palavra-chave pub na definição da função para designar search como parte da API pública da nossa biblioteca. Agora temos uma crate de biblioteca que podemos usar da nossa crate binária e que podemos testar!

Agora precisamos trazer o código definido em src/lib.rs para o escopo do binary crate em src/main.rs e chame-o, conforme mostrado na Listagem 12-14.

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

// --snip--
use minigrep::search;

fn main() {
    // --snip--
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

// --snip--


struct Config {
    query: String,
    file_path: String,
}

impl Config {
    fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("not enough arguments");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}
Listing 12-14: Usando em src/main.rs a função search do crate de biblioteca minigrep

Adicionamos uma linha use minigrep::search para trazer a função search de a crate da biblioteca no escopo da crate binária. Então, na função run, em vez de imprimir o conteúdo do arquivo, chamamos search função e passe o valor config.query e contents como argumentos. Então, run usará um loop for para imprimir cada linha retornada de search que correspondeu à consulta. Este também é um bom momento para remover as chamadas println! em a função main que exibia a consulta e o caminho do arquivo para que nosso o programa imprime apenas os resultados da pesquisa (se nenhum erro ocorrer).

Observe que a função de pesquisa coletará todos os resultados em um vetor ele retorna antes de qualquer impressão acontecer. Esta implementação poderá ser lenta exibir resultados ao pesquisar arquivos grandes, porque os resultados não são impressos como eles são encontrados; discutiremos uma possível maneira de corrigir isso usando iteradores em Capítulo 13.

Uau! Foi muito trabalhoso, mas nos preparamos para o sucesso no futuro. Agora é muito mais fácil lidar com erros e tornamos o código mais modular. Quase todo o nosso trabalho será feito em src/lib.rs daqui em diante.

Vamos aproveitar esta nova modularidade fazendo algo que tem sido difícil com o código antigo, mas é fácil com o novo código: vamos escreva alguns testes!