A formátum a parser szerződése

Egy konfiguráció feldolgozása nem a split meghívásával kezdődik. Először azt döntjük el, mely szöveg számít érvényes adatnak. Ezen az útvonalon soronként egy kulcs és érték szerepel, az első egyenlőségjel választja el őket. A szélső whitespace elhagyható, az üres sor és a trimelés után kettőskereszttel kezdődő teljes sor pedig megjegyzés. Az értéken belüli kettőskereszt közönséges karakter. Idézőjelekhez és escape-szekvenciákhoz itt nem rendelünk külön jelentést.

A cél most a szöveg felbontása, nem a teljes konfiguráció elfogadása. Az ismétlődő kulcsok vizsgálatát később végezzük; a tokenizáló egyetlen sort lát. Előfeltétel a String és Unicode lecke, valamint az Option és a Result használata. Mindhárom megoldás külön fordítható program, ezért egyenként másold őket a projekt fő forrásába.

Miért kölcsönzött nézet?

Ha a bemenet már érvényes UTF-8 szövegként rendelkezésre áll, a kulcsot és az értéket gyakran nem kell lemásolni. A &str az eredeti adatok egy részére mutat; a trim és a split_once ilyen nézeteket ad. A tulajdonos továbbra is az eredeti String marad. A visszaadott nézetek nem használhatók a forrás megszűnése után, és a forrás módosítását is korlátozhatják, amíg a nézetek használatban vannak.

A str dokumentációja szerint a választott elválasztó első előfordulásánál bonthatunk. Ez illeszkedik a szerződésünkhöz: a jel=a=b sor értéke a=b. Ha minden egyenlőségjelnél darabolnánk, külön szabály kellene a részek visszaillesztésére. A szűkebb művelet egyszerűbb hibakezelést és kevesebb elveszíthető információt jelent.

Próbáld ki a szerződést kézzel is

Képzelj el egy felhasználót, aki egy könyvtár elérési útját szeretné beállítani. Az érték szóközt vagy ékezetet is tartalmazhat, ezért a teljes sor whitespace menti darabolása nem megfelelő. Egy név értékeként a Réka Júlia szövegnek egyetlen mezőnek kell maradnia. A szélső whitespace levágása ettől külön művelet: az elválasztó mellé tett igazító szóközöket eltávolítjuk, a szöveg belsejében állókat megőrizzük. Ezt az elvárást a feladat megkezdése előtt mondd ki.

Másik példa a keresési feltétel tárolása. Ha az értékben további egyenlőségjel szerepel, az első bontás utáni teljes maradékra szükség van. Egy több darabos split eredményéből könnyű csak az első két részt felhasználni, és így észrevétlenül levágni az adat végét. Itt nem az a cél, hogy minden lehetséges formátumot elfogadjunk, hanem hogy az általunk vállalt formátum minden elfogadott karakterét megtartsuk.

A kulcsokat ebben a kis nyelvben nem alakítjuk kisbetűssé, és Unicode-normalizálást sem végzünk. Az eltérő írásmód tehát eltérő szöveg marad. Ha később egy üzleti szabály néhány ismert kulcsra korlátozza a bemenetet, azt külön validáló rétegben érdemes vizsgálni. A tokenizáló feladata a szöveghatárok megállapítása; nem kell tudnia, hány munkaszál megengedett vagy létezik-e a megadott könyvtár.

Végül tervezd meg a tesztjeid sorrendjét. Kezdd egy szabályos rekorddal, utána változtass meg rajta egyszerre egy tulajdonságot: tűnjön el az elválasztó, ürüljön ki a kulcs, majd az érték. Így egy hibás tesztből világosan látszik, mely szabály sérült. A kommentekkel és üres sorokkal körbevett rekord pedig külön ellenőrzi, hogy a forráspozíciót nem a sikeres rekordok darabszámából találod ki.

1. feladat: válaszd le a figyelmen kívül hagyható sorokat

Írj függvényt, amely a trimelt adatsort adja vissza, az üres és teljes megjegyzéssorokra pedig None értéket ad. Ne távolíts el soron belüli megjegyzést, mert a nyelvtanunkban ilyen nincs. Külön ellenőrizd a beljebb kezdődő kettőskeresztet és az értékben szereplő kettőskeresztet. Ez a lépés kizárólag osztályoz; még ne utasítsa el az egyenlőségjel nélküli adatsort.

Megoldás
fn adatsor(sor: &str) -> Option<&str> {
    let tiszta = sor.trim();
    if tiszta.is_empty() || tiszta.starts_with('#') {
        None
    } else {
        Some(tiszta)
    }
}

fn main() {
    assert_eq!(adatsor(" \t "), None);
    assert_eq!(adatsor("  # beállítások"), None);
    assert_eq!(adatsor(" cimke = a#b "), Some("cimke = a#b"));
    assert_eq!(adatsor(" hibás sor "), Some("hibás sor"));
    let eredeti = String::from(" név=Réka ");
    assert_eq!(adatsor(&eredeti), Some("név=Réka"));
    assert_eq!(eredeti, " név=Réka ");
}

A függvény nem ígéri, hogy az eredmény már feldolgozott rekord. Így a hívó tudja megkülönböztetni a kihagyható sort a hibás adattól. A névben szereplő ékezet nem igényel kézi bájtindexelést: az elválasztás és a trimelés érvényes szöveghatárokkal dolgozik.

2. feladat: két kölcsönzött mező

Bontsd fel az adatsort az első egyenlőségjelnél, majd mindkét mezőt trimeld. A hiányzó elválasztó, az üres kulcs és az üres érték legyen külön hiba. Ebben a függvényben a megjegyzéssorokat már a hívó kiszűrte. Az érték további egyenlőségjeleit és belső whitespace karaktereit őrizd meg. Gondold végig, miért más szerződés az üres érték elfogadása, mint annak alapértelmezett értékkel helyettesítése.

Megoldás
#[derive(Debug, PartialEq)]
enum Hiba { Elvalaszto, UresKulcs, UresErtek }

fn par(sor: &str) -> Result<(&str, &str), Hiba> {
    let (kulcs, ertek) = sor.split_once('=').ok_or(Hiba::Elvalaszto)?;
    let (kulcs, ertek) = (kulcs.trim(), ertek.trim());
    if kulcs.is_empty() { return Err(Hiba::UresKulcs); }
    if ertek.is_empty() { return Err(Hiba::UresErtek); }
    Ok((kulcs, ertek))
}

fn main() {
    assert_eq!(par(" jel = a=b "), Ok(("jel", "a=b")));
    assert_eq!(par(" név = Réka Júlia "), Ok(("név", "Réka Júlia")));
    assert_eq!(par("nincs"), Err(Hiba::Elvalaszto));
    assert_eq!(par(" = 1"), Err(Hiba::UresKulcs));
    assert_eq!(par("limit= \t"), Err(Hiba::UresErtek));
}

3. feladat: őrizd meg az eredeti sorszámot

Készíts rekordlistát egy teljes szövegből. Minden rekordban legyen egytől induló eredeti sorszám, kulcs és érték. A sorszámozás még a megjegyzések és üres sorok kihagyása előtt történjen. Hibás mező esetén az első hibás eredeti sort add vissza; ne adj részleges listát sikeres eredményként. A rekordok szövege továbbra is a bemenetből kölcsönzött nézet legyen.

Megoldás
#[derive(Debug, PartialEq)]
struct Rekord<'a> { sor: usize, kulcs: &'a str, ertek: &'a str }

fn rekordok(bemenet: &str) -> Result<Vec<Rekord<'_>>, (usize, &'static str)> {
    let mut eredmeny = Vec::new();
    for (index, sor) in bemenet.lines().enumerate() {
        let sor = sor.trim();
        if sor.is_empty() || sor.starts_with('#') { continue; }
        let (kulcs, ertek) = sor.split_once('=').ok_or((index + 1, "hiányzó ="))?;
        let (kulcs, ertek) = (kulcs.trim(), ertek.trim());
        if kulcs.is_empty() || ertek.is_empty() {
            return Err((index + 1, "üres mező"));
        }
        eredmeny.push(Rekord { sor: index + 1, kulcs, ertek });
    }
    Ok(eredmeny)
}

fn main() {
    let forras = String::from("# próba\n\nnév = Réka\njel = a=b\n");
    let lista = rekordok(&forras).unwrap();
    assert_eq!(lista.len(), 2);
    assert_eq!(lista[0], Rekord { sor: 3, kulcs: "név", ertek: "Réka" });
    assert_eq!(lista[1], Rekord { sor: 4, kulcs: "jel", ertek: "a=b" });
    assert_eq!(rekordok("# fejléc\n\nhibás"), Err((3, "hiányzó =")));
    assert!(rekordok("\n# csak megjegyzés\n").unwrap().is_empty());
}

A lista maga foglal memóriát, tehát a kölcsönzött mezők nem teszik a teljes feldolgozást allokációmentessé. Ha a forrást azonnal el kell dobni vagy a rekordot más tulajdonoshoz adod, a String mezők indokoltabbak lehetnek. A könyv szövegkezelési fejezete segít megkülönböztetni a saját adatot és annak szeletét.

Elfogadási feltételek

  • A teljes megjegyzést kihagyod, az értékben lévő kettőskeresztet és további egyenlőségjelet megtartod.
  • A három mezőhiba felismerhető, és hibás bemenetből nem születik sikeres, részleges rekordlista.
  • Az eredeti sorszám az üres és kommentes sorok mellett is pontos, a mezők a bemenet érvényességéhez kötöttek.

Továbblépés

A nyelvtani döntéseket érdemes előbb példabemenetekben rögzíteni, és csak utána függvényekre bontani. Egy későbbi idézőjeles formátum új nyelvtani szabályokat és teszteket igényelne; a mai parserhez ne feltételezz ilyen képességet. A következő leckében a hibákat sorszámmal és valódi forráslánccal írjuk le. A kvíz előtt saját szavaiddal is fogalmazd meg, mi marad a tokenizálás után a teljes konfiguráció ellenőrzésére.