Ein Microservice mit Rust

18.08.2023

Um zu sehen, wie Rust sich im Bereich Microservices schlägt, habe ich einen kleinen Webservice entwickelt. Den Service, sowie die Erfahrungen, die ich bei der Entwicklung gemacht habe, schildere ich in diesem Beitrag.

Rust im Web

Ursprünglich kommt Rust aus dem Bereich der System- und Embedded-Entwicklung. Im Jahr 2018 entschied sich die Community, die Developer Experience in den Bereichen Programmierung von Commandline Interfaces, WebAssembly, Netzwerkdienste und dem Embedded-Bereich zu verbessern.

Doch auch für den Serverbereich gibt es immer mehr Tools, wie beispielsweise die Webframeworks Rocket und Actix, Tokio für asynchrone Programmierung (insbesondere für IO-intensive Anwendungen geeignet), Diesel für Datenbankzugriffe, oder Serde für die Serialisierung und Deserialisierung von Daten in Formate wie beispielsweise JSON und YAML.

Webservice mit Web-UI und REST-API

Der Service, den ich entwickelt habe, beinhaltet sowohl eine Web-UI, als auch eine REST-API. Er heißt KennzD[1] und liefert Informationen zu deutschen Kfz-Kennzeichen. Gibt man beispielsweise „FN“ ein, erhält man die folgenden Informationen (hier das von der REST-API zurückgelieferte JSON):

1 {
2    „kuerzel“: „FN“,
3    „stadt“: „Bodenseekreis“,
4    „sitz“: „Friedrichshafen“,
5    „bundesland“: „Baden-Württemberg“,
6    „vergabe“: „seit 1973“
7 }

Umgesetzt habe ich den Service mithilfe der Frameworks Rocket und Serde.

Für die Web-UI bringt Rocket einen integrierten Templating-Mechanismus mit, der zwei Implementierungen zur Auswahl bereithält: Tera und Handlebars. Für meinen Service habe Handlebars verwendet. Aufgrund des geringen Datenvolumens werden die Daten in einer Map im Speicher gehalten. Eine Datenbankanbindung habe ich daher nicht implementiert.

Im Folgenden beschreibe ich die Hauptbestandteile, aus denen das Programm besteht, etwas näher.

Raketenstart

Der Code, der den Rocket-Service startet, steht in der main.rs und sieht wie folgt aus:

1 #[launch]
2 fn rocket() -> _ {
3    rocket::build()
4        .manage(KennzdRepo::create())
5        .attach(Template::fairing())
6        .mount(„/“, routes![ui::index, ui::ui])
7        .mount(„/api“, routes![api::kennz_by_kuerzel, api::kennz_all])
8        .register(„/api“, catchers![api::not_found])
9 }

Zeile 1: Das #[launch]-Makro generiert eine main-Funktion, welche die mit rocket::build() erzeugte Rocket-Anwendung startet.

Zeile 4: Per „manage“-Funktion kann man der Rocket-Anwendung Objekte übergeben, die man für die Erledigung der Aufgaben des Services benötigt (sog. „managed state“). Hier wird das Repository für die Kennzeichen übergeben, die in der UI sowie über die REST-API ausgeliefert werden.

Zeile 5: Initialisierung der Templating-Engine.

Zeilen 6 und 7: hier werden die Handler, also die Methoden in den Modulen ui und api, an die Pfade „/“ bzw. „/api“ gebunden.

Zeile 8: Registrierung eines Handlers für die Behandlung nicht gefundener Ressourcen.

Die REST API

Die REST-API besteht aus drei einfachen Methoden. Der Code dafür befindet sich in der Datei api.rs.

Alle Kennzeichen

Die erste Funktion, die mit <Basis-URL>/kennzeichen aufgerufen wird, gibt alle Kennzeichen zurück:

1 #[get(„/kennzeichen“)]
2 pub fn kennz_all(repo: &State<KennzdRepo>) -> Json<Vec<Kennzeichen>> {
3     Json(repo.find_all())
4 }

Zeile 1: Makro-Anweisung, welche die Funktion als HTTP GET-Methode unter dem Pfad „/kennzeichen“ deklariert.

Zeile 2: Der Parameter „repo“ ist das von Rocket verwaltete Kennzeichen-Repository, das dem Framework beim Start der Anwendung per „manage“-Funktion übergeben wurde (siehe erstes Codebeispiel). Obwohl der Parameter vom Typ State ist, kann man die Funktionen des Repositries so aufrufen, als wäre das Objekt direkt vom Typ KennzdRepo. Rückgabetyp der Funktion ist eine Liste („Vec“) von Kennzeichen im JSON-Format.

Zeile 3: Der Ausdruck repo.find_all() liefert alle Kennzeichen zurück. Das Ergebnis wird in ein Json-Struct aus dem Serde-Framework gepackt. Dies bewirkt, dass die Kennzeichen-Liste in ein JSON-Array serialisiert wird.

Ein einzelnes Kennzeichen

Die zweite Funktion liefert die Kennzeichendaten für ein bestimmtes Kürzel zurück:

1 #[get(„/kennzeichen/<kuerzel>“)]
2 pub fn kennz_by_kuerzel(kuerzel: &str, repo: &State<KennzdRepo>) -> Option<Json<Kennzeichen>> {
3     match repo.find(kuerzel) {
4         Some(k) => Some(Json(k.clone())),
5         None => None
6     }
7 }

Zeile 1: Kennzeichnung als GET-Methode unter „kennzeichen/“ mit dem Pfad-Parameter „kuerzel“. Aufrufe der URL <Basis-URL>/kennzeichen/<kuerzel> werden an die API-Funktion „kennz_by_kuerzel(…)“ weitergeleitet.

Zeile 2: Neben dem Repo-Stateobjekt wird auch das im Pfad-Parameter <kuerzel> angegebene Kürzel übergeben.

Zeile 3: Im Repo wird das gesuchte Kürzel abgefragt und ein Pattern-Matching auf das Ergebnis angewendet.

Zeile 4: Ein Eintrag für das Kürzel wurde im Repo gefunden. Dieser wird nach JSON konvertiert und zurückgegeben.

Zeile 5: Im Repo existiert kein Eintrag für das Kürzel. In diesem Fall liefert die Methode None zurück.

404 Not found

Die letzte Methode behandelt den Fall, dass das gesuchte Kürzel nicht gefunden wurde:

1 #[catch(404)]
2 pub fn not_found() -> Value {
3     json!({
4         „status“: „error“,
5         „reason“: „Resource not found.“
6     })
7 }

Gibt eine API-Methode None zurück (siehe Abschnitt „Ein einzelnes Kennzeichen“), antwortet Rocket standardmäßig mit einer 404-HTML-Seite im Body.

Die not_found()-Methode sorgt dafür, dass die Antwort stattdessen eine JSON-Struktur im Body enthält. Das json!-Makro von Serde (Zeile 3) wandelt das als Literal definierte JSON in ein JSON-Value-Objekt um, das bei 404-Fehlern an den Client zurückgeschickt wird.

UI mit Handlebars

Um die Template-Engine Handlebars für Rocket zu aktivieren, ist der folgende Dependency-Eintrag in der Cargo.toml vorzunehmen:

[dependencies.rocket_dyn_templates]
version = „=0.1.0-rc.3“
features = [„handlebars“]

Die Hauptfunktionalität des Backend-Codes für die UI befindet sich in der Datei ui.rs und sieht wie folgt aus:

 1 #[get(„/ui?<kuerzel>“)]
 2 pub fn ui(kuerzel: Option<String>, repo: &State<KennzdRepo>) -> Template    {
 3    match kuerzel {
 4        None => render(&Kennzeichen::empty()),
 5        Some(k) => {
 6            match repo.find(&k.to_uppercase()) {
 7                Some(kennz) => render(kennz),
 8                None => render(&Kennzeichen::n_a(k))
 9            }
10        }
11    }
12 }
13
14 fn render(kennz: &Kennzeichen) -> Template {
15     Template::render(„ui“, context! {
16         kennzeichen: kennz
17     })
18 }

Zeile 1: Aufrufe der URL <Basis-URL>/ui?kuerzel=… mit dem URL-Parameter „kuerzel“ werden an die ui-Funktion weitergeleitet.

Zeile 2: Der URL-Parameter „kuerzel“ wird vom Rocket-Framework als Methodenparameter übergeben. Da er als Option deklariert ist, muss der Parameter in der URL nicht zwingend vorhanden sein. Wenn er fehlt, hat das Argument den Wert None.
Der zweite Parameter „repo“ ist wieder das von Rocket verwaltete Kennzeichen-Repository.

Zeile 3: Auf die URL-Variable „kuerzel“ wird ein Pattern Matching gemacht.

Zeile 4: Wurde die URL ohne den „kuerzel“-Parameter aufgerufen, wird die Render-Funktion mit einem „leeren“ Kennzeichen aufgerufen (alle Felder haben einen Leerstring als Wert).

Zeile 5 / 6: Ist ein Kürzel gegeben, wird dieses im Repo abgefragt, und auf das Ergebnis wiederum ein Pattern Matching angewendet.

Zeile 7: Ein Eintrag im Repo wurde für das Kürzel gefunden. Die render-Funktion wird mit dem gefundenen Kennzeichen aufgerufen.

Zeile 8: Im Repo existiert kein Eintrag für das gesuchte Kürzel. Die render-Funktion wird mit einem „Dummy“-Kennzeichen aufgerufen, dessen Felder außer dem Kürzel alle den Wert „n/a“ haben.

Zeile 14: Die render(…)-Funktion, die das übergebene Kennzeichen in das „ui“-Template rendert. Als Kontext wird der Template-Engine das zu rendernde Kennzeichen-Struct „kennz“ unter dem Key „kennzeichen“ mitgegeben.

Das Template

Das „ui“-Template aus der zuvor genannten Render-Funktion liegt im „templates“-Ordner unterhalb des Projektverzeichnisses, und heißt „ui.html.hbs“. Die Endung „.hbs“ steht für „Handlebars“, die im Projekt verwendete Templating-Engine.

Inhalt der Datei ist HTML mit von Handlebars zu ersetzende Template-Ausdrücken.

Das Eingabefeld für das gesuchte Kürzel sieht wie folgt aus:

1 <input  name=“kuerzel“ type=“text“
2         value=“{{#if kennzeichen }}{{ kennzeichen.kuerzel }}{{else}}“{{/if}}“
3         autofocus onFocus=“this.select();“/>
4 <input type=“submit“ value=“Suchen“/>

Ist das „kennzeichen“-Objekt im Rendering-Kontext vorhanden, wird das Kürzel als vorbelegter Wert in das Eingabefeld eingetragen; ansonsten wird ein Leerstring gesetzt.

Die Tabelle für die Ergebnisanzeige sieht wie folgt aus:

1 <table>
2     <tr>
3         <td>K&uuml;rzel:</td>
4         <td>{{ kennzeichen.kuerzel }}</td>
5     </tr>
6     <tr>
7        <td>Stadt / Kreis:</td>
8        <td>{{ kennzeichen.stadt }}</td>
9     </tr>
10    …

Die Tabelle hat jeweils eine Spalte für die Überschrift (Zeilen 3 und 7) und eine für das jeweils anzuzeigende Datenattribut (Zeilen 4 und 8). Für die restlichen Attribute sind entsprechend weitere Tabellenzeilen definiert (hier nicht dargestellt).

Die Developer Experience

Das Arbeiten mit Rocket, Serde und Handlebars hat viel Spaß gemacht und fühlte sich für mich so ähnlich an wie die Entwicklung mit Spring Boot. Dank der Annotationen – bzw. Makros – musste ich nur wenig Code selbst schreiben. Dafür passiert sehr viel „unter der Haube“.

Wer sich für den kompletten Code der Anwendung interessiert, kann ihn sich auf GitHub unter https://github.com/swa-ds/kennzd-rust anschauen.

Verweise

[1] https://kennzd.fly.dev/ – man möge mir das nicht vorhandene Styling der Web-UI verzeihen; aber ich wollte meine Zeit in die Rust-Implementierung investieren, statt in ein „fancy“ Frontend 😉

Zurück zur Übersicht

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

*Pflichtfelder

*