From 8770694e11cbd4311225ddf04d28c681ba81dd99 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 10 Jul 2022 03:30:48 +0100 Subject: [PATCH] add fluent templates example --- .github/workflows/linux.yml | 3 +- Cargo.toml | 8 ++ databases/diesel/Cargo.toml | 4 +- databases/mongodb/Cargo.toml | 4 +- templating/fluent/Cargo.toml | 16 ++++ templating/fluent/README.md | 33 +++++++ templating/fluent/locales/en/main.ftl | 8 ++ templating/fluent/locales/fr/main.ftl | 8 ++ templating/fluent/src/lang_choice.rs | 40 ++++++++ templating/fluent/src/main.rs | 127 +++++++++++++++++++++++++ templating/fluent/templates/error.html | 10 ++ templating/fluent/templates/index.html | 11 +++ templating/fluent/templates/user.html | 12 +++ 13 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 templating/fluent/Cargo.toml create mode 100644 templating/fluent/README.md create mode 100644 templating/fluent/locales/en/main.ftl create mode 100644 templating/fluent/locales/fr/main.ftl create mode 100644 templating/fluent/src/lang_choice.rs create mode 100644 templating/fluent/src/main.rs create mode 100644 templating/fluent/templates/error.html create mode 100644 templating/fluent/templates/index.html create mode 100644 templating/fluent/templates/user.html diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2b3ae97..8f22439 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -50,8 +50,7 @@ jobs: timeout-minutes: 30 with: command: check - # TODO: remove exclude protobuf-example when upgraded; currently fails on nightly - args: --workspace --bins --examples --tests --exclude protobuf-example + args: --workspace --bins --examples --tests - name: start redis uses: supercharge/redis-github-action@1.1.0 diff --git a/Cargo.toml b/Cargo.toml index b3349b3..b9cb6ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ members = [ "server-sent-events", "shutdown-server", "templating/askama", + # "templating/fluent", "templating/handlebars", "templating/sailfish", "templating/tera", @@ -60,4 +61,11 @@ members = [ exclude = [ # uses incompatible libsqlite-sys to other examples "databases/diesel", + + # takes infinity time to compile for some unknown reason + "templating/fluent", ] + +# [patch.crates-io] +# fluent-templates = { git = "https://github.com/robjtede/fluent-templates.git", branch = "fix-circ-dep" } +# fluent-template-macros = { git = "https://github.com/robjtede/fluent-templates.git", branch = "fix-circ-dep" } diff --git a/databases/diesel/Cargo.toml b/databases/diesel/Cargo.toml index 3653229..a050b00 100644 --- a/databases/diesel/Cargo.toml +++ b/databases/diesel/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" [dependencies] actix-web = "4" -diesel = { version = "1.4.8", features = ["sqlite", "r2d2"] } +diesel = { version = "1.4", features = ["sqlite", "r2d2"] } dotenv = "0.15" -env_logger = "0.9.0" +env_logger = "0.9" log = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/databases/mongodb/Cargo.toml b/databases/mongodb/Cargo.toml index 386d06a..a9e0ccb 100644 --- a/databases/mongodb/Cargo.toml +++ b/databases/mongodb/Cargo.toml @@ -5,5 +5,5 @@ edition = "2021" [dependencies] actix-web = "4" -mongodb = "2.0.0" -serde = { version = "1.0", features = ["derive"] } +mongodb = "2" +serde = { version = "1", features = ["derive"] } diff --git a/templating/fluent/Cargo.toml b/templating/fluent/Cargo.toml new file mode 100644 index 0000000..c55a270 --- /dev/null +++ b/templating/fluent/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "templating-fluent" +version = "1.0.0" +edition = "2021" + +[dependencies] +actix-web = "4" +actix-web-lab = "0.16" +fluent-templates = { version = "0.7", features = ["handlebars"] } +handlebars = { version = "4.3", features = ["dir_source"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[patch.crates-io] +fluent-templates = { git = "https://github.com/robjtede/fluent-templates.git", branch = "fix-circ-dep" } +fluent-template-macros = { git = "https://github.com/robjtede/fluent-templates.git", branch = "fix-circ-dep" } diff --git a/templating/fluent/README.md b/templating/fluent/README.md new file mode 100644 index 0000000..f2dc5e8 --- /dev/null +++ b/templating/fluent/README.md @@ -0,0 +1,33 @@ +# Fluent Templates + +This is an example of how to integrate [Fluent Templates](https://crates.io/crates/fluent-templates) with Actix Web using the Handlebars templating engine. + +# Directory Structure + +- `src`: Rust source code for web server and endpoint configuration +- `templates`: Handlebars templates (no text content is stored in the templates) +- `locales`: Fluent files containing translations for English (en) and French (fr) + +## Usage + +```sh +cd templating/fluent +cargo run +``` + +After starting the server, you may visit the following pages: + +- http://localhost:8080 +- http://localhost:8080/Alice/documents +- http://localhost:8080/Bob/passwords +- http://localhost:8080/some-non-existing-page - 404 error rendered using template + +This example implements language selection using the standard Accept-Language header, which is sent by browsers according to OS/browser settings. To view the translated pages, pass the Accept-Encoding header with `en` or `fr`. Values which do not have associated translation files will fall back to English. + +``` +# using HTTPie +http :8080/Alice/documents Accept-Language:fr + +# using cURL +curl http://localhost:8080/Alice/documents -H 'accept-language: fr' +``` diff --git a/templating/fluent/locales/en/main.ftl b/templating/fluent/locales/en/main.ftl new file mode 100644 index 0000000..8b9b034 --- /dev/null +++ b/templating/fluent/locales/en/main.ftl @@ -0,0 +1,8 @@ +home-title = Fluent Templates Example +home-description = This is an example of how to use Fluent templates with Actix Web. + +user-title = { $name }'s Homepage +user-welcome = Welcome back, { $name } +user-data = Here's your { $data }. + +error-not-found = Page not found diff --git a/templating/fluent/locales/fr/main.ftl b/templating/fluent/locales/fr/main.ftl new file mode 100644 index 0000000..f92f9de --- /dev/null +++ b/templating/fluent/locales/fr/main.ftl @@ -0,0 +1,8 @@ +home-title = Un Exemple de Fluent Templates +home-description = Ceci est un exemple d'utilisation des modèles Fluent avec Actix Web. + +user-title = La page d'accueil de { $name } +user-welcome = Bienvenue, { $name } +user-data = Voici votre { $data }. + +error-not-found = Page non trouvée diff --git a/templating/fluent/src/lang_choice.rs b/templating/fluent/src/lang_choice.rs new file mode 100644 index 0000000..028d227 --- /dev/null +++ b/templating/fluent/src/lang_choice.rs @@ -0,0 +1,40 @@ +use std::{ + convert::Infallible, + future::{ready, Ready}, +}; + +use actix_web::{dev, http::header::AcceptLanguage, FromRequest, HttpMessage as _, HttpRequest}; +use fluent_templates::LanguageIdentifier; +use serde::Serialize; + +/// A convenient extractor that finds the clients's preferred language based on an Accept-Language +/// header and falls back to English if header is not found. Serializes easily in Handlebars data. +#[derive(Debug, Serialize)] +#[serde(transparent)] +pub struct LangChoice(String); + +impl LangChoice { + pub(crate) fn from_req(req: &HttpRequest) -> Self { + let lang = req + .get_header::() + .and_then(|lang| lang.preference().into_item()) + .map(|lang| lang.to_string()) + .map_or_else(|| "en".to_owned(), |lang| lang.to_string()); + + Self(lang) + } + + pub fn lang_id(&self) -> LanguageIdentifier { + // unwrap: lang ID should be valid given extraction method + self.0.parse().unwrap() + } +} + +impl FromRequest for LangChoice { + type Error = Infallible; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _pl: &mut dev::Payload) -> Self::Future { + ready(Ok(Self::from_req(req))) + } +} diff --git a/templating/fluent/src/main.rs b/templating/fluent/src/main.rs new file mode 100644 index 0000000..8732a35 --- /dev/null +++ b/templating/fluent/src/main.rs @@ -0,0 +1,127 @@ +use std::io; + +use actix_web::{ + body::BoxBody, + dev::ServiceResponse, + get, + http::{header::ContentType, StatusCode}, + middleware::{ErrorHandlerResponse, ErrorHandlers}, + web, App, HttpResponse, HttpServer, Responder, Result, +}; +use actix_web_lab::{extract::Path, respond::Html}; +use fluent_templates::{static_loader, FluentLoader, Loader as _}; +use handlebars::Handlebars; +use serde_json::json; + +mod lang_choice; +use self::lang_choice::LangChoice; + +static_loader! { + static LOCALES = { + locales: "./locales", + fallback_language: "en", + + // removes unicode isolating marks around arguments + // you typically should only set to false when testing. + customise: |bundle| bundle.set_use_isolating(false), + }; +} + +#[get("/")] +async fn index(hb: web::Data>, lang: LangChoice) -> impl Responder { + let data = json!({ "lang": lang }); + let body = hb.render("index", &data).unwrap(); + Html(body) +} + +#[get("/{user}/{data}")] +async fn user( + hb: web::Data>, + Path(info): Path<(String, String)>, + lang: LangChoice, +) -> impl Responder { + let data = json!({ + "lang": lang, + "user": info.0, + "data": info.1 + }); + let body = hb.render("user", &data).unwrap(); + Html(body) +} + +#[actix_web::main] +async fn main() -> io::Result<()> { + // Handlebars uses a repository for the compiled templates. This object must be shared between + // the application threads, and is therefore passed to the App in an Arc. + let mut handlebars = Handlebars::new(); + + // register template dir with Handlebars registry + handlebars + .register_templates_directory(".html", "./templates") + .unwrap(); + + // register Fluent helper with Handlebars registry + handlebars.register_helper("fluent", Box::new(FluentLoader::new(&*LOCALES))); + + let handlebars = web::Data::new(handlebars); + + HttpServer::new(move || { + App::new() + .wrap(error_handlers()) + .app_data(web::Data::clone(&handlebars)) + .service(index) + .service(user) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await +} + +// Custom error handlers, to return HTML responses when an error occurs. +fn error_handlers() -> ErrorHandlers { + ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found) +} + +// Error handler for a 404 Page not found error. +fn not_found(res: ServiceResponse) -> Result> { + let lang = LangChoice::from_req(res.request()).lang_id(); + let error = LOCALES.lookup(&lang, "error-not-found").unwrap(); + + let response = get_error_response(&res, &error); + + Ok(ErrorHandlerResponse::Response(ServiceResponse::new( + res.into_parts().0, + response.map_into_left_body(), + ))) +} + +// Generic error handler. +fn get_error_response(res: &ServiceResponse, error: &str) -> HttpResponse { + let req = res.request(); + let lang = LangChoice::from_req(req); + + // Provide a fallback to a simple plain text response in case an error occurs during the + // rendering of the error page. + + let hb = req + .app_data::>() + .expect("correctly set up handlebars in app data"); + + let data = json!({ + "lang": lang, + "error": error, + "status_code": res.status().as_str() + }); + + let body = hb.render("error", &data); + + match body { + Ok(body) => HttpResponse::build(res.status()) + .content_type(ContentType::html()) + .body(body), + + Err(_) => HttpResponse::build(res.status()) + .content_type(ContentType::plaintext()) + .body(error.to_string()), + } +} diff --git a/templating/fluent/templates/error.html b/templating/fluent/templates/error.html new file mode 100644 index 0000000..4a3b512 --- /dev/null +++ b/templating/fluent/templates/error.html @@ -0,0 +1,10 @@ + + + + + {{ error }} + + +

{{ status_code }} {{ error }}

+ + diff --git a/templating/fluent/templates/index.html b/templating/fluent/templates/index.html new file mode 100644 index 0000000..9148362 --- /dev/null +++ b/templating/fluent/templates/index.html @@ -0,0 +1,11 @@ + + + + {{ fluent "home-title" }} + + + +

{{ fluent "home-title" }}

+

{{ fluent "home-description" }}

+ + diff --git a/templating/fluent/templates/user.html b/templating/fluent/templates/user.html new file mode 100644 index 0000000..d6468da --- /dev/null +++ b/templating/fluent/templates/user.html @@ -0,0 +1,12 @@ + + + + + {{ fluent "user-title" name=user }} + + + +

{{ fluent "user-welcome" name=user }}

+

{{ fluent "user-data" data=data }}

+ +