Cél és előfeltétel

Ebben a leckében a sorok előkészítését és érvényességvizsgálatát iterátorokkal kapcsolod össze. Előfeltétel a kölcsönzött tokenek, a típusos feldolgozási hibák és a soronkénti feldolgozás. A példák most memóriában megadott szöveggel dolgoznak, ezért a döntések fájlrendszer és parancssori környezet nélkül ellenőrizhetők.

A formátum soronként kulcs=érték. A sor és a mezők szélső whitespace karaktereit levágjuk; az üres sorokat és a trim után # jellel kezdődő teljes kommenteket kihagyjuk. Az elválasztás csak az első egyenlőségjelnél történik. Üres kulcs vagy üres érték hiba. Az ismétlődő kulcsok gyűjteményszintű ellenőrzését a végső parser végzi; ebben a leckében először a soronkénti ellenőrzés viselkedését tesszük világossá.

A szűrés ne veszítse el az eredetet

A sorszámot a nyers sorok felsorolásakor adjuk hozzá. Ha előbb kiszűrnénk a kommenteket, az utána képzett index már nem a felhasználó dokumentumának sorát jelentené. Ez különösen zavaró hibajelzés lenne több üres vagy magyarázó sor után. A feldolgozási lánc tehát nem csupán értéket, hanem az érték eredetét is hordozza.

Az iterátorok általában lusták: a map létrehozása önmagában még nem végezte el az ellenőrzéseket. Az eredményt egy fogyasztó művelet használja fel. Az első hibánál megálló Result-gyűjtés és az összes eredményt vizsgáló ciklus eltérő felhasználói élményt ad. Egyik sem általánosan jobb; az alkalmazás szerződésének kell meghatároznia, hogy egyetlen javítandó hibát vagy teljes hibalistát szeretnénk.

1. feladat: készíts sorszámos, érdemi sorokat

Írj iterátort, amely csak az érdemi sorokat adja tovább, eredeti, egytől induló sorszámmal. Ellenőrizd, hogy a bevezető üres sor és a komment nem számozza át az adatokat. A sor belsejében lévő # jel maradjon közönséges adat: itt nincs sorvégi kommentnyelv.

Megoldás
fn meaningful(text: &str) -> impl Iterator<Item = (usize, &str)> {
    text.lines().enumerate().filter_map(|(index, raw)| {
        let line = raw.trim();
        (!line.is_empty() && !line.starts_with('#')).then_some((index + 1, line))
    })
}
fn main() {
    let result: Vec<_> = meaningful("\n # komment\n a=1\n\n b=x#y\n").collect();
    assert_eq!(result, vec![(3, "a=1"), (5, "b=x#y")]);
    assert!(meaningful(" \n# csak komment\n").next().is_none());
}

A sorszám még a szűrés előtt keletkezik. A visszaadott szöveg a bemenet szelete, ezért nem másoljuk minden sor bájtjait új Stringbe. Ettől a későbbi Vec természetesen saját elemtárolót használ. A kölcsönzött eredmények a forrásszöveg életéhez kötöttek; egy ideiglenes szöveg megszűnése után ez a gyűjtemény sem tarthat érvényes hivatkozásokat.

2. feladat: állj meg az első hibánál

Készíts kulcs-érték párt split_once segítségével, majd gyűjts Result<Vec<_>, _> eredménybe. A hiba az eredeti sorszámot is hordozza. Ellenőrizd, hogy az értéken belüli második egyenlőségjel megmarad, a hibás harmadik sor pedig a harmadik sorként jelenik meg.

Megoldás
fn pair(line: &str) -> Result<(&str, &str), &'static str> {
    let (key, value) = line.split_once('=').ok_or("elvalaszto")?;
    let (key, value) = (key.trim(), value.trim());
    if key.is_empty() { return Err("ures kulcs"); }
    if value.is_empty() { return Err("ures ertek"); }
    Ok((key, value))
}
fn validate(text: &str) -> Result<Vec<(usize, &str, &str)>, (usize, &'static str)> {
    text.lines().enumerate()
        .map(|(index, line)| (index + 1, line.trim()))
        .filter(|(_, line)| !line.is_empty() && !line.starts_with('#'))
        .map(|(number, line)| pair(line)
            .map(|(key, value)| (number, key, value))
            .map_err(|why| (number, why)))
        .collect()
}
fn main() {
    assert_eq!(validate(" path = a=b \n"), Ok(vec![(1, "path", "a=b")]));
    assert_eq!(validate("a=1\n# komment\nhibas\nb=2"), Err((3, "elvalaszto")));
    assert_eq!(validate(" =x"), Err((1, "ures kulcs")));
    assert_eq!(validate("a= "), Err((1, "ures ertek")));
}

Az Err nem válik hiányzó elemmé: a gyűjtés az első elutasításnál hibával tér vissza. Ez azt is jelenti, hogy a későbbi sorokat már nem ellenőrizte a függvény. A felületnek ezért nem szabad teljes dokumentumra vonatkozó „minden más helyes” állítást tennie. A korábbi sikeres elemek helyi gyűjteménye nem részleges sikeres konfigurációként kerül vissza a hívóhoz.

3. feladat: gyűjtsd össze az összes hibát

Más felhasználói igényhez készíts teljes bejárást. A helyes sorok és a hibák külön listában térjenek vissza, eredeti sorrendben és sorszámmal. Egy hibás sor ne akadályozza a későbbi ellenőrzést. A részleges eredményt nevezd részlegesnek: amíg hiba van, ne tekintsd alkalmazható teljes konfigurációnak.

Megoldás
fn validate_all(text: &str) -> (Vec<(usize, &str, &str)>, Vec<(usize, &'static str)>) {
    let mut good = Vec::new();
    let mut errors = Vec::new();
    for (index, raw) in text.lines().enumerate() {
        let line = raw.trim();
        if line.is_empty() || line.starts_with('#') { continue; }
        let result = line.split_once('=').ok_or("elvalaszto")
            .and_then(|(key, value)| {
                let (key, value) = (key.trim(), value.trim());
                if key.is_empty() { Err("ures kulcs") }
                else if value.is_empty() { Err("ures ertek") }
                else { Ok((key, value)) }
            });
        match result {
            Ok((key, value)) => good.push((index + 1, key, value)),
            Err(why) => errors.push((index + 1, why)),
        }
    }
    (good, errors)
}
fn main() {
    let (good, errors) = validate_all("a=1\nhibas\n# komment\n=x\nb=2\n");
    assert_eq!(good, vec![(1, "a", "1"), (5, "b", "2")]);
    assert_eq!(errors, vec![(2, "elvalaszto"), (4, "ures kulcs")]);
}

A két lista kettős eredményét tudatosan vállaljuk. A hívó például megjelenítheti a hibákat anélkül, hogy a helyes részeket éles beállításként alkalmazná. A filter_map(Result::ok) rövidebbnek tűnne, de eltüntetné az elutasítás okát és helyét. A kényelmes adapter helyett előbb mindig azt válaszd ki, milyen információt szabad elveszíteni.

Ellenőrzés és mellékhatás külön maradjon

A validáció közben nem írunk fájlt és nem módosítunk működő konfigurációt. Az első hibánál megálló lánc előtt végrehajtott külső változás ugyanis nem vonódna vissza attól, hogy a collect végül Err-rel tért vissza. A Result a visszatérés típusát szabályozza, nem automatikus tranzakciókezelő.

A teljes hibalista méretét egy szolgáltatásban szintén korlátozni kellene. Ha minden sor hibás, a diagnosztika tárolása is memóriát igényel. Ebben a helyi gyakorlatban kicsi, beágyazott bemenettel dolgozunk. A következő réteg feladata lenne meghatározni, hány hibát, mekkora sort és mennyi összes bemenetet fogadunk el.

Elfogadási feltételek

  1. A kommentek és üres sorok kihagyása után az eredeti harmadik és ötödik sorszám megmarad.
  2. Az érték további egyenlőségjele adat marad, az üres mezők és hiányzó elválasztó hibát adnak.
  3. Az első hibás és a teljes hibalistás mód eltérő, dokumentált eredményt ad, mellékhatás nélkül.

Összefoglaló és továbblépés

A determinisztikus riportok leckében a már ellenőrzött adatok stabil megjelenítését készítjük el. Ott a sorrend és a duplikált kulcsok megőrzési szabálya újabb külön szerződés lesz.