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
- A kommentek és üres sorok kihagyása után az eredeti harmadik és ötödik sorszám megmarad.
- Az érték további egyenlőségjele adat marad, az üres mezők és hiányzó elválasztó hibát adnak.
- 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.