A hibás adatnak is van szerkezete

A parser nemcsak helyes rekordot állít elő: meg kell mondania azt is, mely bemenetet miért utasította el. Ha minden problémából ugyanaz a szöveg lesz, a hívó csak üzenetek összehasonlításával tudna dönteni. Ez törékeny felület, mert egy nyelvi javítás az alkalmazás viselkedését is megváltoztathatná. Ebben a leckében a hiba fajtája, eredeti sorszáma és az alatta keletkezett valódi hiba külön szerepet kap.

Az előző leckében rögzített kulcs=érték formátumból indulunk. A szerkezeti ellenőrzés továbbra is az első egyenlőségjelet keresi, majd tiltja az üres mezőket. Egyes értékeknél ezen felül számot várunk; ez már szemantikai követelmény. Attól, hogy egy sor felbontható, a hozzá tartozó beállítás még lehet értelmetlen. A két réteg külön kezelése pontosabb teszteket és javítási lehetőségeket ad.

Válaszd szét a hibafajtát és a megjelenítést

A saját enum változatai a program által felismerhető esetek. A Display megvalósítása emberi szöveget készít, az Error pedig más hibákhoz kapcsolhatja az eredményt. Egy egyszerű belső feldolgozónál a Debug és PartialEq is elég lehet a tesztekhez; a nyilvános könyvtári határon már hasznos a szokásos hibatrait-ek megvalósítása. Ne adj alapértelmezett értéket hibás adat helyett pusztán azért, hogy kényelmesebb legyen a visszatérési típus.

Az eredeti sorszám nem ugyanaz, mint az érvényes rekordok száma. A kommentek és az üres sorok ugyanúgy elfoglalnak egy sort a felhasználó fájljában. A pozíciót ezért a feldolgozás elején rögzítjük, majd együtt visszük a hibával. A hibajelzésnek nem kell a teljes konfigurációt vagy annak minden értékét tartalmaznia. A következő példákban a sor és a probléma típusa már elég az eligazodáshoz.

Gondolkodj a hívó döntéseiben

Tegyük fel, hogy a konfigurációt egy parancssori program olvassa. Hiányzó egyenlőségjelnél a felhasználó szerkesztheti a megadott sort. Egy értelmezhető, de tiltott számnál már mást kell javítania: a program által megengedett tartományból választ. A két helyzethez eltérő súgó tartozhat, miközben a két hiba ugyanazon közös felső szintű típusba csomagolható. A lényeg, hogy a részletes esetek ne tűnjenek el a közös felület mögött.

Az olvasó kódnak nem kell minden alsó szintű implementációt ismernie. Elég lehet a sorszám és egy nyilvános hibafajta, miközben a forráslánc megőrzi a mélyebb okot az ellenőrzéshez. Ez a szétválasztás akkor is hasznos, ha később más számformátumot vagy más bemeneti adaptert használsz. A hívó a saját számára fontos szerződésre támaszkodik, nem a belső könyvtár egy adott verziójának hibaüzenetére.

A kérdőjel operátor nem teszi automatikusan helyessé ezt a felületet. Megállítja a sikeres út további végrehajtását és a megfelelő visszatérési formába juttatja a hibát, de a szükséges kontextust nekünk kell hozzáadnunk. A példákban a sorszámot a map_err lezárás viszi a csomagoló típusba. Ha előtte szöveggé alakítanánk az eredeti hibát, a későbbi source már nem ugyanahhoz a típusos okhoz vezetne.

Teszteléskor különítsd el a szerződés változatlan és szerkeszthető részeit. A sorszám és a hibafajta pontosan ellenőrizhető, a felhasználói mondatot viszont csak akkor kösd teljes egyezéshez, ha annak formátuma valóban publikus követelmény. A példaadatokon egyszerre egy hibát vezess be; több rossz sor esetén pedig külön vizsgáld, hogy tényleg az első forrásbeli hiba érkezik meg, és a későbbi érvényes rekord nem fordítja sikerre az egész feldolgozást.

1. feladat: típusos szerkezeti hibák

Készíts sorfelbontót, amely siker esetén kölcsönzött kulcsot és értéket ad. Hibában legyen sorszám és külön változat a hiányzó elválasztóra, üres kulcsra és üres értékre. Teszteld a hiba adattartalmát, ne a Debug formátum pontos szövegét. Ezzel a megjelenítés később változhat anélkül, hogy az esetek közti programozott különbség eltűnne. A bemeneti sorszámot a hívó adja át.

Megoldás
#[derive(Debug, PartialEq)]
enum Fajta { Elvalaszto, UresKulcs, UresErtek }
#[derive(Debug, PartialEq)]
struct Hiba { sor: usize, fajta: Fajta }

fn bont(sor: usize, szoveg: &str) -> Result<(&str, &str), Hiba> {
    let (k, v) = szoveg.split_once('=').ok_or(Hiba { sor, fajta: Fajta::Elvalaszto })?;
    let (k, v) = (k.trim(), v.trim());
    if k.is_empty() { return Err(Hiba { sor, fajta: Fajta::UresKulcs }); }
    if v.is_empty() { return Err(Hiba { sor, fajta: Fajta::UresErtek }); }
    Ok((k, v))
}

fn main() {
    assert_eq!(bont(7, "rossz"), Err(Hiba { sor: 7, fajta: Fajta::Elvalaszto }));
    assert_eq!(bont(8, " = x"), Err(Hiba { sor: 8, fajta: Fajta::UresKulcs }));
    assert_eq!(bont(9, "k= "), Err(Hiba { sor: 9, fajta: Fajta::UresErtek }));
    assert_eq!(bont(10, " jel=a=b "), Ok(("jel", "a=b")));
}

2. feladat: különítsd el a számhibát a tartományhibától

Egy workers beállításnál a megengedett érték egy és harminckettő közötti u8 szám. Először a trimelt értéket alakítsd számmá, utána vizsgáld az üzleti tartományt. A nem értelmezhető vagy u8-ban nem ábrázolható szám ParseIntError eredetét őrizd meg. A nulla és a harminchárom ezzel szemben ábrázolható, de tiltott érték: más hibafajtát igényel. A választott standard parse szabályait használjuk, saját számnyelvtant itt nem írunk.

Megoldás
use std::{error::Error, fmt, num::ParseIntError};

#[derive(Debug)]
enum WorkersHiba { Szam(ParseIntError), Tartomany(u8) }
impl fmt::Display for WorkersHiba {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Szam(_) => write!(f, "nem értelmezhető workers szám"),
            Self::Tartomany(n) => write!(f, "a workers értéke nincs 1 és 32 között: {n}"),
        }
    }
}
impl Error for WorkersHiba {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self { Self::Szam(e) => Some(e), Self::Tartomany(_) => None }
    }
}
fn workers(szoveg: &str) -> Result<u8, WorkersHiba> {
    let n = szoveg.trim().parse::<u8>().map_err(WorkersHiba::Szam)?;
    if !(1..=32).contains(&n) { return Err(WorkersHiba::Tartomany(n)); }
    Ok(n)
}
fn main() {
    assert_eq!(workers(" 4 ").unwrap(), 4);
    assert!(matches!(workers("0"), Err(WorkersHiba::Tartomany(0))));
    assert!(matches!(workers("33"), Err(WorkersHiba::Tartomany(33))));
    assert!(matches!(workers("300"), Err(WorkersHiba::Szam(_))));
    let hiba = workers("sok").unwrap_err();
    assert!(hiba.source().is_some());
    assert!(workers("0").unwrap_err().source().is_none());
}

A ParseIntError dokumentációja a szövegből számmá alakítás hibáját írja le. A tartományellenőrzést nekünk kell hozzáadnunk. A harminckettő itt a saját szerződésünk, nem a Rust vagy a számítógép szálainak általános korlátja.

3. feladat: állj meg az első hibás rekordon

Dolgozz fel több olyan kulcs=érték sort, amelyben most u16 számot várunk értékként. Üres sorokat és teljes kommenteket hagyj ki, de a forrás sorszámát őrizd meg. A szerkezeti hiba és a számkonverzió hibája maradjon külön eset. Sikerben az összes sorszám és szám jelenjen meg; hiba esetén a részleges vektor ne kerüljön ki sikeres eredményként. Ez az első hibánál megálló stratégia, a hibák összegyűjtése későbbi döntés lesz.

Megoldás
use std::{error::Error, fmt, num::ParseIntError};
#[derive(Debug)]
enum Fajta { Szerkezet, Szam(ParseIntError) }
#[derive(Debug)]
struct SorHiba { sor: usize, fajta: Fajta }
impl fmt::Display for SorHiba {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "a(z) {}. sor nem megfelelő", self.sor)
    }
}
impl Error for SorHiba {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.fajta { Fajta::Szam(e) => Some(e), Fajta::Szerkezet => None }
    }
}
fn szamok(bemenet: &str) -> Result<Vec<(usize, u16)>, SorHiba> {
    let mut eredmeny = Vec::new();
    for (index, sor) in bemenet.lines().enumerate() {
        let s = sor.trim();
        if s.is_empty() || s.starts_with('#') { continue; }
        let sor = index + 1;
        let (k, v) = s.split_once('=').ok_or(SorHiba { sor, fajta: Fajta::Szerkezet })?;
        if k.trim().is_empty() || v.trim().is_empty() {
            return Err(SorHiba { sor, fajta: Fajta::Szerkezet });
        }
        let n = v.trim().parse::<u16>().map_err(|e| SorHiba { sor, fajta: Fajta::Szam(e) })?;
        eredmeny.push((sor, n));
    }
    Ok(eredmeny)
}
fn main() {
    assert_eq!(szamok("# kezdet\n\na=2\nb=3").unwrap(), vec![(3, 2), (4, 3)]);
    let hiba = szamok("a=2\nb=nincs\nc=4").unwrap_err();
    assert_eq!(hiba.sor, 2);
    assert!(hiba.source().is_some());
    let szerkezeti = szamok("# kezdet\n=4").unwrap_err();
    assert_eq!(szerkezeti.sor, 2);
    assert!(matches!(szerkezeti.fajta, Fajta::Szerkezet));
}

Az Error::source az eredeti okhoz vezető kapcsolatot adja; nem a Display üzenetéből állítja vissza azt. A forráshibát ezért addig típusosan visszük, amíg a felhasználói felület nem dönt annak megjelenítéséről. Az itt eldobott ideiglenes vektor elkerüli a részleges siker félreértelmezését, de önmagában nem jelent tranzakciót külső rendszerek között.

Elfogadási feltételek

  • A szerkezeti hibákra közvetlenül enumváltozat alapján lehet dönteni, pontos eredeti sorszámmal.
  • A számkonverzió hibája és a saját megengedett tartomány megsértése külön eset, és a konverzió forrása megmarad.
  • Hibás több soros bemenetnél az első hibát kapod, nem egy sikeresnek jelölt részleges listát.

Továbblépés

A jó hibatípus a hívónak használható döntési felületet ad, miközben az emberi üzenet szerkeszthető marad. Ne a teszt kedvéért formázd az egész hibát szöveggé, ha a változat és a sor közvetlenül vizsgálható. A következő leckében az olvasás maga is hibázhat. Ott az adatfolyam hibáját ugyanilyen gondosan választjuk el a tartalmi problémáktól.