Cél és előfeltétel
A miniprojekt egy kis konfigurációs nyelv teljes feldolgozását kapcsolja össze. Előfeltétel az iterátoros validáció, a determinisztikus riport és az első modul típusos hibakezelése. A bemenet memóriában kapott &str; a megoldások nem olvasnak fájlt, és külön edition 2024 programként futtathatók.
A szabályaink pontosak: az üres és teljes soros kommentek kimaradnak, a többi sor az első egyenlőségjelnél válik ketté. A kulcs és érték széléről whitespace kerül le, belső tartalmuk megmarad. Üres mező és duplikált kulcs hiba. A hibák az eredeti, egytől induló sorszámot hordozzák. Az érték további egyenlőségjele és a sor belsejében lévő # jel közönséges adat.
A sikeres konfiguráció ne legyen félkész
A parser először saját helyi térképet épít. Az első hibánál Err-rel tér vissza, és nem ad ki alkalmazható részkonfigurációt. Ez nem jelenti, hogy a hiba után következő sorokat is ellenőrizte. A felületnek ezért egy javítandó hibát mutatunk, nem teljes, minden problémát felsoroló diagnosztikát. A későbbi többhibás mód más visszatérési szerződést igényelne.
A térkép kulcsai és értékei a bemeneti szöveget kölcsönzik. Ez kényelmes egy rövid feldolgozási életciklusban, de a forrást életben kell tartani, amíg a konfigurációt használjuk. Nem másolunk a tanulóprogramban feleslegesen Stringet minden mezőhöz, ugyanakkor a BTreeMap saját tárolója továbbra is foglal. A felület nem állít allokációmentességet vagy korlátlan bemenet kezelhetőségét.
1. feladat: a sorhiba oka legyen típusos
Készíts Kind enumot a hiányzó elválasztó, üres kulcs és üres érték hibájához. A sorfeldolgozó adjon kölcsönzött párt vagy konkrét Kind értéket. A várt hibák ne magyar mondatok szövegegyezésére épüljenek; a megjelenítés később külön rétegben fordíthatja a hibafajtát felhasználói üzenetté.
Megoldás
#[derive(Debug, PartialEq, Eq)]
enum Kind { MissingEquals, EmptyKey, EmptyValue }
fn pair(line: &str) -> Result<(&str, &str), Kind> {
let (key, value) = line.split_once('=').ok_or(Kind::MissingEquals)?;
let (key, value) = (key.trim(), value.trim());
if key.is_empty() { return Err(Kind::EmptyKey); }
if value.is_empty() { return Err(Kind::EmptyValue); }
Ok((key, value))
}
fn main() {
assert_eq!(pair(" a = x=y "), Ok(("a", "x=y")));
assert_eq!(pair(" a = x#y "), Ok(("a", "x#y")));
assert_eq!(pair("hibas"), Err(Kind::MissingEquals));
assert_eq!(pair("=x"), Err(Kind::EmptyKey));
assert_eq!(pair("a= "), Err(Kind::EmptyValue));
}
A split_once a hiányzó elválasztót külön jelzi; a trim után két további szabály következik. Ha mindkét mező üres, az elsőként vizsgált kulcs hibáját kapjuk. Ez tudatos ellenőrzési sorrend, nem a program véletlen mellékhatása. A teljes dokumentum kommentkezelése nem ebbe a függvénybe került: azt a sor feldolgozásának megkezdése előtt döntjük el.
2. feladat: a duplikáció két forráshelyét őrizd meg
Készíts bejegyzést értékkel és eredeti sorszámmal. Második azonos kulcsnál a hiba tartalmazza az új sor és az első bejegyzés sorának számát is. A korábbi érték ne változzon meg. Ugyanaz az érték újra megadva is duplikáció; itt nem „utolsó nyer” konfigurációs nyelvet tervezünk.
Megoldás
use std::collections::{BTreeMap, btree_map::Entry};
#[derive(Debug, PartialEq, Eq)]
struct Duplicate { line: usize, first_line: usize }
struct Item<'a> { value: &'a str, line: usize }
fn add<'a>(map: &mut BTreeMap<&'a str, Item<'a>>, line: usize,
key: &'a str, value: &'a str) -> Result<(), Duplicate> {
match map.entry(key) {
Entry::Occupied(old) => Err(Duplicate { line, first_line: old.get().line }),
Entry::Vacant(slot) => { slot.insert(Item { value, line }); Ok(()) }
}
}
fn main() {
let mut map = BTreeMap::new();
add(&mut map, 2, "port", "8080").unwrap();
assert_eq!(add(&mut map, 5, "port", "9090"),
Err(Duplicate { line: 5, first_line: 2 }));
assert_eq!(map.get("port").unwrap().value, "8080");
assert_eq!(map.get("port").unwrap().line, 2);
assert_eq!(map.len(), 1);
}
A diagnosztika a két releváns helyet kapcsolja össze, ezért a felhasználó mindkét előfordulást megtalálhatja. A térképben tárolt sorszám a rendezéstől nem változik meg. A példa a már ellenőrzött mezők felvételét mutatja; az add önmagában nem helyettesíti a korábbi sorvalidációt, mert a feladat rétegeit tudatosan külön készítettük el.
3. feladat: teljes parser és rendezett riport
Kapcsold össze a sorok eredeti számozását, a kommentek kihagyását, a típusos hibákat és a duplikációellenőrzést. A sikeres riport kulcs szerint rendezett legyen. Vizsgáld az üres dokumentumot, a kommenteket, az értékben megmaradó jeleket, valamint minden hibafajtát. Ne használj előre létező fájlt vagy környezeti konfigurációt.
Megoldás
use std::collections::BTreeMap;
use std::fmt::Write;
#[derive(Debug, PartialEq, Eq)]
enum Kind { MissingEquals, EmptyKey, EmptyValue, Duplicate { first_line: usize } }
#[derive(Debug, PartialEq, Eq)]
struct Error { line: usize, kind: Kind }
#[derive(Debug, PartialEq, Eq)]
struct Item<'a> { value: &'a str, line: usize }
fn parse(text: &str) -> Result<BTreeMap<&str, Item<'_>>, Error> {
let mut values = BTreeMap::<&str, Item<'_>>::new();
for (index, raw) in text.lines().enumerate() {
let number = index + 1;
let line = raw.trim();
if line.is_empty() || line.starts_with('#') { continue; }
let (key, value) = line.split_once('=')
.ok_or(Error { line: number, kind: Kind::MissingEquals })?;
let (key, value) = (key.trim(), value.trim());
if key.is_empty() { return Err(Error { line: number, kind: Kind::EmptyKey }); }
if value.is_empty() { return Err(Error { line: number, kind: Kind::EmptyValue }); }
if let Some(first) = values.get(key) {
return Err(Error { line: number,
kind: Kind::Duplicate { first_line: first.line } });
}
values.insert(key, Item { value, line: number });
}
Ok(values)
}
fn render(values: &BTreeMap<&str, Item<'_>>) -> String {
let mut report = String::new();
for (key, item) in values {
writeln!(&mut report, "{key}={}", item.value).unwrap();
}
report
}
fn main() {
let input = "\n # megjegyzes\n b = 2\n a = one=two \n";
let values = parse(input).unwrap();
assert_eq!(values.get("b").unwrap().line, 3);
assert_eq!(values.get("a").unwrap().line, 4);
assert_eq!(render(&values), "a=one=two\nb=2\n");
assert_eq!(parse("x=one#two").unwrap().get("x").unwrap().value, "one#two");
assert!(parse("\n # komment\n").unwrap().is_empty());
assert_eq!(parse("a=1\n\n a=2"), Err(Error { line: 3,
kind: Kind::Duplicate { first_line: 1 } }));
assert_eq!(parse("=x"), Err(Error { line: 1, kind: Kind::EmptyKey }));
assert_eq!(parse("x= "), Err(Error { line: 1, kind: Kind::EmptyValue }));
assert_eq!(parse("# komment\nhibas"), Err(Error { line: 2, kind: Kind::MissingEquals }));
}
A teljes eredmény csak az utolsó sikeres sor után válik Ok értékké. A rendezett riport nem tartalmaz kommentet és eredeti whitespace elrendezést, ezért nem alkalmas a dokumentum bájtpontos visszaállítására. A sorszámok a feldolgozott adatok mellett megmaradnak, de a normalizált riportban szándékosan nem jelennek meg, mert az egy külön kimeneti formátum.
A nyelv határait ne bővítsd csendben
Az érték belsejében lévő # nem komment, és a további egyenlőségjelet megőrizzük. Idézőjelezést, escape szabályt, környezeti változó behelyettesítését vagy több soros értéket nem vezettünk be. Ezek hozzáadása új nyelvi szabály lenne, amelyhez a hibák, a riport és a tesztek módosítása is szükséges. Ne nevezd a példát egy létező összetett konfigurációs formátum teljes implementációjának.
Az alkalmazás akkor módosítsa működő beállításait, amikor már sikeres teljes parsereredményt kapott és az értékek üzleti ellenőrzése is megtörtént. A port szöveg például ettől még nem biztosan szabályos portszám. A jelenlegi parser a nyelvi szerkezetet ellenőrzi; az alkalmazás mezőinek típusa és megengedett tartománya egy következő, külön ellenőrizhető feldolgozási lépés.
Elfogadási feltételek
- Az eredeti harmadik és negyedik sorhely megmarad, a kommentek és üres sorok kimaradnak.
- Az üres mezők, hiányzó elválasztó és duplikált kulcs pontos típusos hibát adnak; duplikációnál mindkét sorhely ismert.
- A riport pontosan a=one=two, majd b=2 sorokat ad, az értéken belüli # és = jelek megőrzésével.
Összefoglaló és továbblépés
A projekt egy kis nyelv teljes, világos szerződésű parserét adja. A következő lépés lehet saját alkalmazási beállítások létrehozása a már ellenőrzött szöveges mezőkből. Ott is válaszd külön a szerkezeti hibát, az üzleti tartományt és a mellékhatást: ettől marad a kód és a felhasználói visszajelzés egyszerre kiszámítható.