Separar a identidade do ativo do tipo de equipamento

RustBeginner
Pratique Agora

Introdução

Uma equipe de campo precisa de duas lanternas funcionando. Duas linhas de leitura podem representar duas lanternas diferentes, uma única lanterna lida duas vezes ou uma em bom estado e outra danificada. O programa precisa preservar essas diferenças antes de concluir se o conjunto de equipamentos está completo.

Neste laboratório, você implementará em Rust um parser de estados e um coletor de ativos. Manterá os objetos físicos distintos, ignorará leituras repetidas idênticas e rejeitará registros contraditórios sem modificar os arquivos de origem. O próximo laboratório usará essas identidades para explicar faltas ou excedentes de cada tipo de equipamento. São registros sintéticos, não inspeções de equipamentos físicos.

Você já deve conhecer estruturas e enumerações Rust, HashMap, Result, leitura de arquivos de texto e laços for básicos. O parser, a interface de comandos e o formatador preparados permitem focar nas decisões de identidade. O autor estima 45 minutos, incluindo leitura e correções comuns; esse tempo não foi medido com iniciantes.

Ler estados sem adivinhar

Nesta etapa, inspecione dois arquivos reais de leituras, preveja seus itens utilizáveis e implemente a pequena decisão de estado que faz funcionar a visualização normal de linhas do programa. Abra Terminal. O projeto preparado está em /home/labex/project/field-kit, com Rust 1.99.1.

O export a seguir acrescenta o diretório do Cargo ao caminho de busca de comandos deste shell. $PATH mantém os diretórios existentes. Isso dura apenas na sessão atual de Terminal; repita em um novo shell. Não altera arquivos de inicialização do shell.

export PATH=/home/labex/.cargo/bin:$PATH

Use cd para selecionar o projeto; repita se perder seu diretório. pwd imprime o diretório atual para você confirmar onde ocorrerão as edições posteriores.

cd /home/labex/project/field-kit
pwd

Espere /home/labex/project/field-kit. Corrija um erro de digitação no caminho com cd, sem redefinir a VM. cat imprime um arquivo sem alterá-lo. Leia estas duas entradas reais:

cat data/two-good.tsv
cat data/one-damaged.tsv

TSV significa valores separados por tabulação. Cada leitura tem três campos: asset_id, type_id, condition, separados por tabulações literais. No primeiro arquivo, os ativos físicos a e b têm estado good. No segundo, a está em bom estado e b está damaged. Confira manualmente: há dois itens físicos em ambos os arquivos, mas apenas o primeiro descreve duas lanternas utilizáveis. As palavras good e damaged são os dois estados exatos permitidos; equipamento danificado é um dado válido, não uma linha de entrada incorreta.

O projeto fornece src/lib.rs com estruturas e leitores TSV, src/main.rs com a interface de comandos e o formatador, e src/readiness.rs para o laboratório posterior. Mantenha esses arquivos, Cargo.toml, rust-toolchain.toml e todos os arquivos originais de data/ inalterados. Aqui você só pode editar src/condition.rs e, na próxima etapa, src/identity.rs, preservando seus nomes e tipos públicos.

Leia o módulo de estados:

cat src/condition.rs

A enumeração e o método de exibição já são fornecidos. impl agrupa métodos de Condition; Self nomeia essa enumeração dentro do bloco. pub expõe esses nomes ao leitor. derive fornece cópia, exibição de depuração e igualdade. parse retorna Result<Self, String>: Ok com um estado ou Err com uma explicação de erro em uma string própria. A implementação provisória atual rejeita todas as palavras, portanto ainda não distingue os dois arquivos normais.

Crie um ponto de restauração apenas do código-fonte antes de editar. mktemp -d cria um diretório novo substituindo os seis caracteres X por um sufixo único; não reutiliza um backup anterior. $() armazena o caminho impresso em uma variável da sessão. && executa cp -p somente após a criação bem-sucedida do diretório; a cópia preserva os metadados do código-fonte. As aspas mantêm unido o caminho da variável. Se uma operação falhar, leia o erro e obtenha um backup bem-sucedido antes de editar:

rs03_before_condition=$(mktemp -d .edit-before-condition.XXXXXX) && cp -p src/condition.rs "$rs03_before_condition/condition.rs"

Abra o módulo existente com nano; -c mostra a posição do cursor:

nano -c src/condition.rs

A notação ^ do nano significa Ctrl. Pressione Ctrl+W, digite pub fn parse e pressione Enter para localizar a função. Leia suas chaves de abertura e fechamento. Use as setas e Backspace/Delete para editar somente o corpo deste método e a vinculação do parâmetro não usado. Renomeie _value para value, preservando o tipo do parâmetro. Preserve a enumeração, derive e o método name.

Escreva uma expressão match value com um ramo exato para cada palavra permitida e um curinga para qualquer outra string. Um ramo como "good" => Ok(Self::Good), mostra a sintaxe. Construa você mesmo o resultado Damaged correspondente. _ corresponde a qualquer valor restante, que deve retornar Err em vez de inventar silenciosamente um estado. format! cria uma String; {value:?} inclui aspas em torno do texto inesperado para que espaços e grafia permaneçam visíveis no diagnóstico. Use isso em uma mensagem explicando que a entrada deve ser good ou damaged. Feche as chaves do match e da função, mantendo o match como resultado do método.

Pressione Ctrl+O e Enter para confirmar src/condition.rs; Ctrl+X sai do nano. Não digite comandos do shell enquanto ainda estiver no editor. Leia a edição salva:

cat src/condition.rs

cargo run compila e inicia o programa preparado. --quiet oculta o progresso do Cargo, e -- passa os argumentos seguintes ao programa. rows valida e exibe cada linha do arquivo real; ainda não elimina duplicatas.

cargo run --quiet -- rows data/two-good.tsv
asset_id	type_id	condition
a	light-a	good
b	light-a	good

A visualização normal de linhas agora deve mostrar os dois IDs e seus estados bons. Compare com a outra origem:

cargo run --quiet -- rows data/one-damaged.tsv
asset_id	type_id	condition
a	light-a	good
b	light-a	damaged

Isso corresponde à sua previsão manual: ainda são dois itens lidos, apenas um em bom estado. Você escreveu uma decisão real no caminho do leitor efetivo, não um programa de demonstração separado.

O contrato de entrada controlada

Leia também o plano real:

cat data/plan.tsv

Seus campos são type_id, name, required. light-a e light-b exibem Flashlight, mas são IDs de tipo distintos. Um nome é texto de exibição, não identidade. Exigências positivas sem leituras representam faltas; exigências zero sem leituras têm falta zero. O leitor fornecido aplica estas regras:

  • IDs começam com uma letra ASCII minúscula, seguida apenas por letras minúsculas, dígitos ou -. Diferenciam maiúsculas e minúsculas.
  • Os estados são exatamente good e damaged; nenhuma outra grafia é aceita.
  • Nomes são ASCII imprimível não vazio, incluindo espaços. Quantidades são dígitos decimais com valor de 0 a 999, permitindo zeros à esquerda. IDs de tipo do plano não podem se repetir, mesmo com conteúdo idêntico.
  • O cabeçalho exato de três campos deve vir primeiro. LF/CRLF, ausência de quebra de linha final e linhas posteriores completamente vazias são permitidos. Linhas contendo apenas espaços em branco não são vazias. Retornos de carro isolados não são finais de linha. Campos não são aparados: espaços em um ID ou estado são erros, enquanto espaços em um nome são preservados.

ASCII aqui significa letras básicas do inglês, dígitos, espaços e pontuação; nomes excluem caracteres de controle. Este é um formato pequeno e definido, não um leitor CSV geral. Seu método de estados fornece a decisão entre dois estados dentro desse leitor preparado.

Recuperar uma edição de código-fonte

Para um erro Rust, leia o arquivo e a linha indicados pelo Cargo, reabra esse arquivo, busque a função afetada e repare a edição. Salve, saia e repita o comando que falhou. Para descartar uma edição acidental ainda não salva, use Ctrl+X e N na pergunta sobre salvar. Isso não desfaz alterações salvas anteriormente.

Somente ao recuperar um erro salvo, restaure seu próprio ponto de controle de estados em vez de reiniciar a preparação. No mesmo Terminal, sua variável ainda identifica o backup. Em um novo Terminal, repita PATH e cd, depois liste os diretórios reais desta fase:

ls -d .edit-before-condition.*

-d lista nomes de diretório em vez de conteúdo; * corresponde aos sufixos gerados. Execute o comando a seguir, cole um nome de diretório exato impresso enquanto o shell aguarda e pressione Enter. read armazena essa linha na variável desta sessão:

read rs03_before_condition

Inspecione o arquivo selecionado. Se o caminho ou a fase estiver errado, repita a seleção e a inspeção; se não houver backup, repare usando a referência em vez de adivinhar um sufixo.

cat "$rs03_before_condition/condition.rs"

Após confirmar o código-fonte anterior à edição desejado, este comando substitui deliberadamente apenas o arquivo de estados permitido:

cp -p "$rs03_before_condition/condition.rs" src/condition.rs

Isso restaura o parser incompleto, portanto implemente novamente a decisão de estado e repita os dois comandos de linhas. A referência abaixo serve para comparar um método específico após sua tentativa, não para substituir o módulo inteiro como operação normal de edição.

Referência completa de estados para comparação e recuperação
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Condition { Good, Damaged }

impl Condition {
    pub fn parse(value: &str) -> Result<Self, String> {
        match value {
            "good" => Ok(Self::Good),
            "damaged" => Ok(Self::Damaged),
            _ => Err(format!("condition must be good or damaged, got {value:?}")),
        }
    }
    pub fn name(self) -> &'static str {
        match self { Self::Good => "good", Self::Damaged => "damaged" }
    }
}

Coletar ativos físicos por ID

Nesta etapa, implemente a coleta por identidade preservando ativos físicos distintos e contando repetições idênticas uma única vez. Continue em Terminal, em /home/labex/project/field-kit. Leia a origem maior:

cat data/scans.tsv

Antes de programar, preveja os itens únicos. O ativo a ocorre duas vezes com o mesmo tipo e estado; a, b e c são três lanternas separadas do tipo light-a. O ativo d pertence a light-b, outro tipo com o mesmo nome de exibição no plano. e e f pertencem ao tipo não planejado other; equipamento extra legítimo não deve desaparecer. Deve haver seis IDs físicos, não sete nem um por tipo ou nome.

Leia a estrutura existente do coletor:

cat src/identity.rs

Scan contém asset_id, type_id, condition e a line de diagnóstico. O use crate::Scan fornecido importa essa estrutura desta biblioteca. A função toma emprestado &[Scan], portanto não deve alterar nem assumir a propriedade dos registros do chamador. Seu Result retorna um vetor de registros únicos ou uma explicação útil de conflito.

Um HashMap pode associar um registro a cada ID de ativo. Inserir uma chave existente substitui seu valor anterior, portanto consulte antes de inserir. Decida separadamente o que fazer com um ID novo, uma repetição idêntica de ID/tipo/estado e uma repetição contraditória. Não compare o Scan inteiro usando igualdade derivada: conteúdo idêntico ocorre naturalmente em números de linha diferentes. Compare tipo e estado; mantenha os dois números de linha apenas para o diagnóstico.

Preserve o código-fonte desta fase antes de editar:

rs03_before_identity=$(mktemp -d .edit-before-identity.XXXXXX) && cp -p src/identity.rs "$rs03_before_identity/identity.rs"
nano -c src/identity.rs

Este código inicial não importa HashMap. Mantenha use crate::Scan; e acrescente a importação da biblioteca padrão ao lado dela, no início:

use std::collections::HashMap;

std é a biblioteca padrão do Rust; crate é a biblioteca deste projeto. Importar um nome o torna disponível neste módulo, não em todos os arquivos. Uma implementação equivalente com mapa ordenado ou ordenação também é permitida, com as importações de que realmente precisar.

Busque pub fn collect_assets com Ctrl+W e pressione Enter. Renomeie _scans para scans, preservando seu tipo de fatia. Implemente as decisões da função; mantenha as outras definições e o módulo de estados concluído inalterados.

Uma abordagem com mapa começa com um HashMap<String, Scan> mutável. Para cada leitura emprestada, get(&scan.asset_id) retorna Some com um registro anterior ou None. if let Some(previous) só entra quando existe um registro. Se o tipo ou o estado divergir, retorne um Err nomeando o ID completo do ativo, a linha atual e a primeira linha. format! pode combinar esses campos em uma mensagem própria. Se o conteúdo concordar, continue pula a inserção dessa repetição.

Para um ID novo, insira uma cópia clonada do ID e do registro; .clone() cria cópias próprias da entrada emprestada. Após examinar cada leitura, mova os registros armazenados para um novo Vec e retorne Ok com esse vetor. Vec::new() cria um vetor vazio e push acrescenta um item. Em um laço como for (_, scan) in by_id, o par é desestruturado e _ ignora a chave. A ordem de iteração do HashMap não é especificada; o formatador fornecido ordena os IDs de ativos para exibição, portanto sua ordem interna não precisa ser fixa.

Salve com Ctrl+O, Enter e Ctrl+X. Inspecione a função salva antes de executar:

cat src/identity.rs

Compare primeiro as linhas brutas reais:

cargo run --quiet -- rows data/scans.tsv

A repetição de a ainda está presente ali. Agora exercite o coletor concluído:

cargo run --quiet -- assets data/scans.tsv
asset_id	type_id	condition
a	light-a	good
b	light-a	good
c	light-a	good
d	light-b	damaged
e	other	good
f	other	damaged

Confirme os seis IDs, tipos e estados. Apenas a repetição idêntica desapareceu da visualização coletada; o arquivo de entrada não foi editado. IDs distintos do mesmo tipo, danos e equipamentos não planejados permaneceram. A próxima etapa verifica contradições, que devem rejeitar todo o resultado coletado em vez de escolher silenciosamente o primeiro ou o último registro.

Para um erro de compilação, repare a linha indicada no nano, incluindo uma importação ausente se esse for o problema. Somente ao desfazer um erro salvo, a recuperação condicional a seguir usa seu próprio arquivo anterior ao coletor. Em um novo Terminal, repita PATH e cd, depois liste os nomes reais de backup da fase:

ls -d .edit-before-identity.*

Execute read, cole um nome de diretório exato impresso e pressione Enter. Confirme seu conteúdo antes de restaurar. Se nenhum backup corresponder, use a referência para um reparo específico em vez de adivinhar um caminho.

read rs03_before_identity
cat "$rs03_before_identity/identity.rs"

Após confirmar o código-fonte desejado, restaure apenas o arquivo de identidade permitido:

cp -p "$rs03_before_identity/identity.rs" src/identity.rs

Isso restaura a implementação provisória original do coletor, incluindo suas importações originais. Acrescente novamente a importação necessária à sua implementação e conclua a função antes de testar outra vez. Isso não remove seu parser de estados bem-sucedido nem altera as entradas brutas.

Referência completa de identidade para comparação e recuperação

Compare a decisão relevante após tentar sua própria implementação e faça um reparo local pelo nano. Este é o único código-fonte completo de identidade usado para preparar as unidades independentes posteriores.

use crate::Scan;
use std::collections::HashMap;

pub fn collect_assets(scans: &[Scan]) -> Result<Vec<Scan>, String> {
    let mut by_id: HashMap<String, Scan> = HashMap::new();
    for scan in scans {
        if let Some(previous) = by_id.get(&scan.asset_id) {
            if previous.type_id != scan.type_id || previous.condition != scan.condition {
                return Err(format!(
                    "line {}: conflicting asset {} (first at line {})",
                    scan.line, scan.asset_id, previous.line
                ));
            }
            continue;
        }
        by_id.insert(scan.asset_id.clone(), scan.clone());
    }
    let mut unique = Vec::new();
    for (_, scan) in by_id {
        unique.push(scan);
    }
    Ok(unique)
}

Rejeitar conflitos e recuperar-se de erros de entrada

Nesta etapa, você distinguirá evidências contraditórias, um estado desconhecido e um caminho incorreto. Continue em Terminal, no diretório do projeto. As próximas falhas são intencionais: devem mostrar um erro e retornar um status de saída diferente de zero. Não apague nem conserte os exemplos de origem preservados para fazer os erros desaparecerem.

Os exemplos de erro abaixo mostram o texto da implementação de referência. Sua mensagem pode usar outras palavras: um conflito deve identificar o caminho real, o ID completo do ativo e ambas as linhas de origem; um estado desconhecido deve identificar o caminho e a linha reais e explicar o valor rejeitado. Reproduzir exatamente o texto não é o objetivo.

Primeiro leia as evidências contraditórias:

cat data/conflict.tsv

Elas descrevem o ativo a como good e damaged ao mesmo tempo. Execute o coletor:

cargo run --quiet -- assets data/conflict.tsv
data/conflict.tsv: line 3: conflicting asset a (first at line 2)

A interface de comandos fornecida acrescenta o caminho real de entrada ao erro do coletor. Nenhuma tabela de sucesso é impressa. Uma implementação que escolhe o último registro ocultaria evidências e poderia mudar o resultado simplesmente invertendo a ordem do arquivo. Repetições idênticas são permitidas, mas contradições exigem investigação fora deste exercício.

Recupere-se escolhendo novamente a origem válida:

cargo run --quiet -- assets data/scans.tsv

Agora leia deliberadamente o exemplo com um estado desconhecido:

cargo run --quiet -- rows data/bad-status.tsv
data/bad-status.tsv: line 2: condition must be good or damaged, got "broken"

É uma decisão de formato, não um ativo danificado: damaged é dado válido, enquanto broken está fora do vocabulário controlado do curso. Leia um registro danificado válido para se recuperar:

cargo run --quiet -- rows data/one-damaged.tsv

Por último, use intencionalmente um nome de arquivo no singular que não existe:

cargo run --quiet -- assets data/scan.tsv

O erro cita data/scan.tsv e informa que o arquivo não foi encontrado. Isso é diferente de um erro de linha dentro de um arquivo existente. Corrija o caminho, mantendo a origem real intacta:

cargo run --quiet -- assets data/scans.tsv

A tabela de seis ativos deve retornar. Se uma edição Rust estiver incorreta, reabra o código-fonte indicado no nano e repare a função afetada. Para desfazer um erro salvo, use o ponto de controle da fase de estados ou identidade e o procedimento explícito de confirmação ls/read/cat ensinado na etapa anterior; restaure apenas esse código-fonte permitido, refaça sua implementação e execute novamente este comando final. Alterações não salvas no nano podem ser descartadas com Ctrl+X e N. Nenhum caso exige executar a preparação novamente ou editar uma entrada protegida. Seu programa agora separa erros de origem de registros repetidos, danificados e extras válidos. O próximo laboratório usará essas identidades únicas para calcular faltas.

Resumo

Você implementou um parser de enumeração com dois estados e coletou equipamentos pelo ID de cada ativo físico. Identidades diferentes foram preservadas, repetições idênticas não aumentaram o resultado e contradições geraram erros com contexto de origem, em vez de substituir evidências silenciosamente. Você também se recuperou de um estado desconhecido e de um nome de arquivo incorreto, mantendo os originais. Esses registros confiáveis permitem explicar faltas e excedentes de cada tipo.