JSON und TOML
Um den Umgang mit Datenformaten wie JSON und TOML anzuschauen werden wir eine Datei vom einen Format ins andere migrieren. Wir stellen uns vor das sei die Konfigurationsdatei eines Texteditors. Die Migration machen wir, weil TOML mehr "human friendly" ist als JSON.
Projektstruktur
Erstelle zuerst ein neues Rust Projekt:
cargo new json2toml
cd json2toml
Als nächstes fügen wir die Bibliotheken serde, serde_json und toml hinzu.
serde ist eine extrem verbreitet Bibliothek, welche für SERialiasieren und DEserialisieren verschiedener Datenformate verwendet wird.
Die Unterstützung für die einzelnen Formate selbst sind in separate Bibliotheken ausgelagert, deshalb brauchen wir noch je eine für JSON und TOML.
Die Dokumentation von serde findest du hier: serde.rs
cargo add serde --features derive
cargo add serde_json toml
Vergiss nicht --features derive!
Das ist eine optionale Funktion von serde, die wir aber unbedingt verwenden wollen.
Nun brauchen wir noch eine Datei, welche wir migrieren wollen.
Speichere den folgenden Inhalt unter config.json (im json2toml Ordner) ab:
{
"showUpdatePopup": false,
"theme": "monokai",
"indentationByLanguage": {
"Rust": 4,
"JavaScript": 2,
"C": 8
}
}
JSON zu einem Rust Datentyp einlesen
Nun definieren wir einen entsprechenden Rust Datentyp:
use std::collections::HashMap;
struct Config {
show_update_popup: bool,
theme: String,
indentation_by_language: HashMap<String, u8>,
}
Um eine Darstellung wie JSON nun in diesen Datentyp einlesen zu können, lassen wir serde für uns den nötigen Code generieren:
use serde::Deserialize;
#[derive(Deserialize)]
struct Config { /**/ }
Dank diesem derive-Makro können wir nun die Funktion serde_json::from_str verwenden:
fn main() {
let json_string = std::fs::read_to_string("config.json").unwrap();
let config: Config = serde_json::from_str(&json_string).unwrap();
println!("theme: {}", config.theme);
}
Mit std::fs::read_to_string lesen wir die Datei ein.
unwrap ist eine Funktion für die Fehlerbehandlung.
Wenn ein Fehler auftaucht, dann bringt unwrap das Programm einfach zum Absturz.
In einem Produktionsreifen Programm würde man natürlich bessere Fehlerbehandlung machen.
Was passiert, wenn dieses Programm mit cargo run ausgeführt wird?
Es gibt folgenden Fehler aus:
missing field
show_update_popup
Wir haben noch das Problem, dass JavaScript / JSON und Rust unterschiedliche Namenskonventionen haben.
In JSON wird oft camelCase verwendent, in Rust ist hingegen snake_case die Konvention.
Dies müssen wir der Bibliothek serde beibringen:
#[derive(Deserialize)]
#[serde(rename_all(deserialize = "camelCase"))]
struct Config { /**/ }
Hier kommt also die Annotation #[serde(rename_all(deserialize = "camelCase"))] hinzu.
Das ist eine Information, die vom darüberliegenden #[derive(Deserialize)] gelesen werden kann.
Dadurch können wir das Verhalten von serde beim Deserialisieren genau steuern.
Wenn du das Programm jetzt nochmal ausführst, dann sollte es klappen.
Ausgabe in TOML
Nun wollen wir die Konfiguration im TOML Format ausgeben.
Der Deserialize Trait funktioniert nur in eine Richtung, für die andere Richtung braucht es den Serialize Trait:
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
struct Config { /**/ }
Und in der main Funktion:
let toml_string = toml::to_string_pretty(&config).unwrap();
print!("{toml_string}");
Toll! Der Weg von JSON zu TOML ist geschafft.
Umbenennen beim Serialisieren
Wir mussten eine Annotation verwenden, um beim Deserialisieren von camelCase zu snake_case zu übersetzen.
Ähnliches funktioniert auch beim Serialisieren.
Angenommen wir wollen den Konfigurationswert show_update_popup noch zu verbose umbenennen.
Das geht ganz einfach:
struct Config {
#[serde(rename(serialize = "verbose"))]
show_update_popup: bool,
// ...
}
Es gibt eine grosse Auswahl solcher Annotationen und man kann sogar eigene Funktionen angeben, welche das Deserialisieren oder Serialisieren übernehmen.
Mit serde hat man immer genau das Verhältnis von Bequemlichkeit und Kontrolle, das man gerade braucht.
Fazit
Makros sind ein mächtiges Instrument. Oft stellen Bibliotheken Makros zur Verfügung, damit die Konsumenten mit wenig Code viel erreichen können.