Implementation eines Konfigurationsparsers mit Hilfe von boost::spirit (Teil 3b)
Als letzten Schritt zur Vervollstaendigung unserer Klassenhierarchie wollen wir nun die Klasse element implementieren. Sie soll die folgenden Use Cases abdecken:
- Interpretation eines element Objekts als group, list oder atom
- Zugriff auf die Child Elemente ueber Key (Gruppe) oder Index (Array und Liste)
- Konvertierung eines Leaf Elements in einen der unterstuetzten Typen
- Lookup eines Leaf Elements ueber Pfadangabe und optionalen Defaultwert
Der erste Punkt ist leicht zu realisieren. Wir schreiben drei Funktionen
const group& as_group() const { return dynamic_cast<const group&>(*this); } const list& as_list() const { return dynamic_cast<const list&>(*this); } const atom& as_atom() const { return dynamic_cast<const atom&>(*this); }
Der dynamic_cast mit einer Objektreferenz sorgt dafuer, dass automatisch eine Exception geworfen wird, wenn das Objekt nicht vom gewuenschten Typ ist. Dies sollte vom Benutzer abgefangen werden.
Auch der Zugriff auf einzelne Childelemente per Key oder Index stellt kein grosses Problem dar. Wir definieren die Indexoperatoren
const element& operator[](const std::string& key) const { return this->as_group().get(key); } const element& operator[](size_t index) const { return this->as_list().get(key); }
Dabei setzen wir voraus, dass der Benutzer weiss, um welchen Typ es sich beim jeweiligen Element handelt und interpretieren es dementsprechend entweder als Gruppe oder Liste. Wird ein Operator auf einem ungeeigneten Datentyp aufgerufen, so werfen die as_group() bzw. as_list() Funktionen entsprechende Exceptions.
Falls es sich bei einem element Objekt um ein Leaf Element handelt, so muessen wir es in einen numerischen Typ oder String umwandeln koennen. Da wir bereits eine entsprechende Funktion in der atom Klasse definiert haben, koennen wir einfach durchschleifen:
template <typename T> const T as() const { return this->as_atom().as<T>(); }
Bleibt nur noch der Use Case des Wertelookups ueber eine Pfadangabe. Nehmen wir an, unsere Konfigurationsdatei sieht folgendermassen aus:
app = { windows = ( { title = "Fenster 1"; width = 400; height = 300; }, { title = "Fenster 2"; width = 600; heigh = 450; } ); };
Der Parser liefert uns ein Root Element, das wir r nennen wollen. Dann soll beispielsweise der Zugriff auf den height Wert des zweiten window Elements moeglich sein ueber
long height = r.lookup<long>("app.windows[1].height", 800);
wobei 800 der Defaultwert ist, wenn die Option nicht gefunden wurde. Weiterhin wollen wir die folgende Syntax erlauben:
long height = r["app.windows[1].height"].as<long>();
Dazu muessen wir spaeter den bereits oben definierten operator[] geeignet anpassen. Implementieren wir aber zunaechst die lookup() Funktion:
typedef std::deque<boost::variant<std::string, unsigned int> > token_list; template <typename T> const T lookup(const std::string& path) const { token_list tokens = util::split(path); return recursive_lookup(*this, tokens).as<T>(); } template <typename T> const element& recursive_lookup(const element& e, token_list tokens) const { if(tokens.empty()) return e; boost::variant<std::string, unsigned int> t = tokens.front(); tokens.pop_front(); const element& next(boost::apply_visitor(visitor(e), t)); return recursive_lookup(next, tokens); }
Wir splitten hier zunaechst den Pfadstring in eine Liste von Tokens, die den einzelnen Zugriffsbezeichnern entspricht (in unserem Beispiel also ["app", "windows", 1, "height"]). Anschliessend rufen wir die Funktion recursive_lookup() mit dem aktuellen Element als Ausgangspunkt auf, die rekursiv durch den Baum traversiert und das letzte Element zurueckliefert, das schliesslich als atom interpretiert und in den gewuenschten Typ konvertiert wird. Die Implementierung der Funktion split sowie der Fall mit Defaultwert ist dem vollstaendigen Quellcode zu entnehmen.
Passen wir abschliessend noch den operator[] an, um den gleichen Lookup Mechanismus auch in der Indexschreibweise zu ermoeglichen:
const element& operator[](const std::string& key) const { if(key.find_first_of('.') != std::string::npos) { token_list tokens = util::split(key); return recursive_lookup(*this, tokens); } else return this->as_group().get(key); }
Damit haben wir nun alle folgenden (aequivalenten) Abfragemoeglichkeiten abgedeckt:
std::string title = r["app.windows[0].title"].as<std::string>(); std::string title = r["app"]["windows"][0]["title"].as<std::string>(); std::string title = r.lookup<std::string>("app.windows[0].title");
Mit Hilfe der im letzten Teil definierten Iteratoren koennen wir nun zum Beispiel auch ueber alle windows Elemente loopen und deren Eigenschaften ausgeben:
const cfg::list& windows = r["app.windows"].as_list(); cfg::list::const_iterator it1 = windows.begin(); for(; it1 != windows.end(); ++it1) { const cfg::group& properties = it1->as_group(); cfg::group::const_iterator it2 = properties.begin(); for(; it2 != properties.end(); ++it2) { std::cout << it2->first << ": " << it2->second->as<std::string>() << std::endl; } }
Die somit definierte Klassenstruktur ermoeglicht einen einfachen und effizienten Zugriff auf die Konfigurationsdaten, ist aber gleichzeitig noch ausreichend performant, um eine schnelle Erzeugung der Baumstruktur vom Parser aus zu gewaehrleisten. Die vollstaendige Implementierung kann hier heruntergeladen werden.
Im naechsten und letzten Teil der Serie vervollstaendigen wir endlich unsere kleine Parser Bibliothek, indem wir semantische Aktionen zum Parser hinzufuegen, um den Parsetree zu erzeugen.
Sehr schönes Beispiel für den spirit Parser ! Habe viel neues Gelernt. Freue mich schon auf den nächsten (letzten?) Teil – und bin neugierig, wie der komplette Source-code zum schnellen Spielen aussieht
Danke !
ben
Hallo Ben,
freut mich, dass dir das kleine Tutorial gefaellt. Im Laufe des Tages werde ich den letzten Teil inklusive komplettem Sourcecode online stellen.
Gruss Johannes