Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 270 additions & 21 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"pointercrate-core",
"pointercrate-core-api",
"pointercrate-core-pages",
"pointercrate-core-macros",
"pointercrate-demonlist",
"pointercrate-demonlist-api",
"pointercrate-demonlist-pages",
Expand Down
2 changes: 2 additions & 0 deletions pointercrate-core-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ sqlx = { version = "0.8", default-features = false, features = [ "runtime-tokio-
log = "0.4.27"
serde_urlencoded = "0.7.0"
maud = "0.27.0"
unic-langid = "0.9.5"
tokio = "1.46.1"
2 changes: 2 additions & 0 deletions pointercrate-core-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub mod error;
pub mod etag;
pub mod localization;
pub mod maintenance;
pub mod pagination;
pub mod preferences;
pub mod query;
pub mod response;
29 changes: 29 additions & 0 deletions pointercrate-core-api/src/localization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use crate::preferences::{ClientPreferences, PreferenceManager};
use crate::{tryo_result, tryo_state};
use pointercrate_core::error::CoreError;
use pointercrate_core::localization::LocaleConfiguration;
use rocket::{
request::{FromRequest, Outcome},
Request,
};
use unic_langid::subtags::Language;

pub const LOCALE_COOKIE_NAME: &str = "locale";

pub struct ClientLocale(pub Language);

#[rocket::async_trait]
impl<'r> FromRequest<'r> for ClientLocale {
type Error = CoreError;

async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let preference_manager = tryo_state!(request, PreferenceManager);
let preferences = ClientPreferences::from_cookies(request.cookies(), preference_manager);
let language = tryo_result!(preferences
.get(LOCALE_COOKIE_NAME)
.ok_or_else(|| CoreError::internal_server_error("locale set not registered with preference manager")));
let lang_id = LocaleConfiguration::get().by_code(language);

Outcome::Success(ClientLocale(lang_id.language))
}
}
10 changes: 6 additions & 4 deletions pointercrate-core-api/src/maintenance.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Module providing a "maintenance mode" fairing (middleware)

use crate::error::Result;
use pointercrate_core::error::CoreError;
use crate::{error::Result, localization::ClientLocale};
use pointercrate_core::{error::CoreError, localization::LANGUAGE};
use rocket::{
fairing::{Fairing, Info, Kind},
http::Method,
Expand Down Expand Up @@ -49,7 +49,9 @@
}
}

// we can't use the #[localized] proc-macro here due to issues related to imports
// (https://github.com/stadust/pointercrate/pull/232#discussion_r2118293806)
#[rocket::get("/maintenance")]
async fn maintenance() -> Result<()> {
Err(CoreError::ReadOnlyMaintenance.into())
async fn maintenance(locale: ClientLocale) -> Result<()> {
LANGUAGE.scope(locale.0, async { Err(CoreError::ReadOnlyMaintenance.into()) }).await

Check warning on line 56 in pointercrate-core-api/src/maintenance.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/maintenance.rs#L55-L56

Added lines #L55 - L56 were not covered by tests
}
87 changes: 87 additions & 0 deletions pointercrate-core-api/src/preferences.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::collections::HashMap;

use crate::localization::LOCALE_COOKIE_NAME;
use pointercrate_core::error::CoreError;
use pointercrate_core::localization::LocaleConfiguration;
use rocket::{
http::CookieJar,
request::{FromRequest, Outcome},
Request,
};

/// A request guard which stores the preferences sent from the client.
pub struct ClientPreferences<'k, 'v>(HashMap<&'k str, &'v str>);

impl<'k: 'v, 'v> ClientPreferences<'k, 'v> {
/// Retrieve a particular preference which was sent to us from the client.
///
/// `T` must implement `From<ClientPreference>`, which [`String`] already
/// implements, in case the untouched cookie value is what needs to be handled.
pub fn get(&self, name: &'k str) -> Option<&'v str> {
self.0.get(name).map(|&s| s)
}

pub fn from_cookies(cookies: &'v CookieJar<'v>, preference_manager: &'k PreferenceManager) -> Self {
ClientPreferences(
preference_manager
.0
.iter()
.map(|(name, default)| {
(
name.as_ref(),
cookies
.get(&format!("preference-{}", name))
.map(|cookie| cookie.value())
.unwrap_or(default)
.as_ref(),
)
})
.collect(),
)
}
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for ClientPreferences<'r, 'r> {
type Error = CoreError;

async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let preference_manager = match request.rocket().state::<PreferenceManager>() {
Some(preference_manager) => preference_manager,
_ => return Outcome::Success(ClientPreferences(HashMap::new())), // return an empty preferences hashmap if this instance doesnt support preferences

Check warning on line 51 in pointercrate-core-api/src/preferences.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/preferences.rs#L48-L51

Added lines #L48 - L51 were not covered by tests
};

let preferences = ClientPreferences::from_cookies(request.cookies(), preference_manager);

Check warning on line 54 in pointercrate-core-api/src/preferences.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/preferences.rs#L54

Added line #L54 was not covered by tests

Outcome::Success(preferences)
}

Check warning on line 57 in pointercrate-core-api/src/preferences.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/preferences.rs#L56-L57

Added lines #L56 - L57 were not covered by tests
}

/// A configuration state to manage all of your pointercrate instance's
/// client preferences.
#[derive(Default)]
pub struct PreferenceManager(HashMap<String, String>);

impl PreferenceManager {
/// Append a new preference to this [`PreferenceManager`]. `name` represents
/// the name of the cookie which stores the value of this preference.
///
/// Note that the cookie name is prefixed with `"preference-"`, so creating a
/// preference with the `name` value as `"theme"` would result in the cookie
/// sent from the client being named `"preference-theme"`.
///
/// If the cookie was not received, its value will default to `default`.
pub fn preference(mut self, name: impl Into<String>, default: impl Into<String>) -> Self {
self.0.insert(name.into(), default.into());

self
}

/// Automatically register the preferences needed to store active locales.
///
/// Requires the global localization context to have been set up via [`LocalesLoader::commit`],
/// otherwise will panic.
pub fn with_localization(self) -> Self {
self.preference(LOCALE_COOKIE_NAME, LocaleConfiguration::get().fallback.as_str())
}

Check warning on line 86 in pointercrate-core-api/src/preferences.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/preferences.rs#L84-L86

Added lines #L84 - L86 were not covered by tests
}
43 changes: 36 additions & 7 deletions pointercrate-core-api/src/response.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
use crate::etag::Tagged;
use maud::{html, DOCTYPE};
use pointercrate_core::etag::Taggable;
use crate::localization::LOCALE_COOKIE_NAME;
use crate::{
etag::Tagged,
preferences::{ClientPreferences, PreferenceManager},
};
use maud::{html, Render, DOCTYPE};
use pointercrate_core::localization::LocaleConfiguration;
use pointercrate_core::{etag::Taggable, localization::LANGUAGE};
use pointercrate_core_pages::{
head::{Head, HeadLike},
PageConfiguration, PageFragment,
};
use rocket::tokio::runtime::Handle;
use rocket::tokio::task::block_in_place;
use rocket::{
http::{ContentType, Header, Status},
response::Responder,
Expand All @@ -30,24 +37,46 @@

impl<'r, 'o: 'r> Responder<'r, 'o> for Page {
fn respond_to(self, request: &'r Request<'_>) -> rocket::response::Result<'o> {
let page_config = request.rocket().state::<PageConfiguration>().ok_or(Status::InternalServerError)?;
let preference_manager = request.rocket().state::<PreferenceManager>().ok_or(Status::InternalServerError)?;
let preferences = ClientPreferences::from_cookies(request.cookies(), preference_manager);

Check warning on line 41 in pointercrate-core-api/src/response.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/response.rs#L40-L41

Added lines #L40 - L41 were not covered by tests

let language = preferences.get(LOCALE_COOKIE_NAME).ok_or(Status::InternalServerError)?;
let lang_id = LocaleConfiguration::get().by_code(language);

Check warning on line 44 in pointercrate-core-api/src/response.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/response.rs#L43-L44

Added lines #L43 - L44 were not covered by tests

let (page_config, nav_bar, footer) = block_in_place(move || {
Handle::current().block_on(async {
LANGUAGE
.scope(lang_id.language, async {
let page_config = request
.rocket()
.state::<fn() -> PageConfiguration>()
.ok_or(Status::InternalServerError)?();

Check warning on line 53 in pointercrate-core-api/src/response.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/response.rs#L46-L53

Added lines #L46 - L53 were not covered by tests

let nav_bar = page_config.nav_bar.render();
let footer = page_config.footer.render();

Check warning on line 56 in pointercrate-core-api/src/response.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/response.rs#L55-L56

Added lines #L55 - L56 were not covered by tests

Ok((page_config, nav_bar, footer))
})
.await
})
})?;

Check warning on line 62 in pointercrate-core-api/src/response.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/response.rs#L58-L62

Added lines #L58 - L62 were not covered by tests

let fragment = self.0;

let rendered_fragment = html! {
(DOCTYPE)
html lang="en" prefix="og: http://opg.me/ns#" {
html lang=(lang_id) prefix="og: http://opg.me/ns#" {

Check warning on line 68 in pointercrate-core-api/src/response.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/response.rs#L68

Added line #L68 was not covered by tests
head {
(page_config.head)
(fragment.head)
}
body {
div.content {
(page_config.nav_bar)
(nav_bar)

Check warning on line 75 in pointercrate-core-api/src/response.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/response.rs#L75

Added line #L75 was not covered by tests
(fragment.body)
div #bg {}
}
(page_config.footer)
(footer)

Check warning on line 79 in pointercrate-core-api/src/response.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-api/src/response.rs#L79

Added line #L79 was not covered by tests
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions pointercrate-core-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "pointercrate-core-macros"
version = "0.1.0"
authors.workspace = true
description.workspace = true
homepage.workspace = true
edition.workspace = true
repository.workspace = true

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0.101", features = ["full"] }
quote = "1.0.40"
65 changes: 65 additions & 0 deletions pointercrate-core-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse2, parse_macro_input, parse_quote, ItemFn};

/// A procedural macro for automatically wrapping a request handler inside a tokio::task_local!
/// [`LocalKey`] scope for `LANGUAGE`, with the value of the [`ClientLocale`] request guard.
///
/// Use of this macro eliminates the need for writing and maintaining boilerplate code caused
/// by manually wrapping the request handler body inside a `LANGUAGE` scope, while also
/// having to take in a [`ClientLocale`] guard and handling that properly.
///
/// This macro should be used for any endpoint whose request handler calls a translation
/// function at some point, so basically any page or API endpoint (API endpoints need
/// to be localized because errors are also translated)
#[proc_macro_attribute]
pub fn localized(_: TokenStream, input: TokenStream) -> TokenStream {
let mut f = parse_macro_input!(input as ItemFn);

// modify the request handler to automatically take in our [`ClientLocale`] request
// guard (defined in pointercrate-core-api/src/localization.rs)
f.sig
.inputs
.push(parse_quote! { __locale: pointercrate_core_api::localization::ClientLocale });

let block = &f.block;
let block = quote! {
{
pointercrate_core::localization::LANGUAGE.scope(__locale.0, async {
#block
}).await
}
};

f.block = parse2(block).unwrap();

TokenStream::from(quote!(#f))
}

/// Identical behaviour to `#[localized]`, but modified to support error catchers.
#[proc_macro_attribute]
pub fn localized_catcher(_: TokenStream, input: TokenStream) -> TokenStream {
let mut f = parse_macro_input!(input as ItemFn);

f.sig.inputs.push(parse_quote! { __request: &rocket::Request<'_> });

let block = &f.block;
let block = quote! {
{
use rocket::request::FromRequest;

let __locale = match pointercrate_core_api::localization::ClientLocale::from_request(__request).await {
rocket::request::Outcome::Success(locale) => locale,
_ => return pointercrate_core_api::error::ErrorResponder::from(pointercrate_core::error::CoreError::internal_server_error("An error occurred while trying to extract requested locale. Check your locale fallbacks!")),
};

pointercrate_core::localization::LANGUAGE.scope(__locale.0, async {
#block
}).await
}
};

f.block = parse2(block).unwrap();

TokenStream::from(quote!(#f))
}
3 changes: 3 additions & 0 deletions pointercrate-core-pages/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ edition.workspace = true

[dependencies]
maud = "0.27.0"
pointercrate-core = {path = "../pointercrate-core"}
unic-langid = "0.9.5"
log = "0.4.27"
11 changes: 7 additions & 4 deletions pointercrate-core-pages/src/footer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use maud::{html, Markup, PreEscaped, Render};
use pointercrate_core::localization::tr;

pub struct Footer {
copyright_notice: Markup,
Expand All @@ -20,15 +21,15 @@
self
}

pub fn with_link(mut self, href: &'static str, text: &'static str) -> Self {
pub fn with_link<T: Into<String>>(mut self, href: &'static str, text: T) -> Self {

Check warning on line 24 in pointercrate-core-pages/src/footer.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-pages/src/footer.rs#L24

Added line #L24 was not covered by tests
self.twitter_links.push(Link::new(href, text));
self
}
}

pub enum FooterColumn {
LinkList { heading: &'static str, links: Vec<Link> },
Arbitrary { heading: &'static str, content: Markup },
LinkList { heading: String, links: Vec<Link> },
Arbitrary { heading: String, content: Markup },
}

pub struct Link {
Expand Down Expand Up @@ -91,7 +92,9 @@
}
div style="display: flex; justify-content: center; align-items: center" {
i class = "fab fa-twitter fa-2x" {}
(PreEscaped("&nbsp;&nbsp;Tweet Us:"))
(PreEscaped(
format!("&nbsp;&nbsp;{}", tr("footer-tweet"))

Check warning on line 96 in pointercrate-core-pages/src/footer.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-pages/src/footer.rs#L96

Added line #L96 was not covered by tests
))
@for link in &self.twitter_links {
(PreEscaped("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"))
a href=(link.href) target="_blank" style = "color:#666" {(link.text)}
Expand Down
1 change: 1 addition & 0 deletions pointercrate-core-pages/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
.script("/static/core/js/ui.js")
.script("/static/core/js/nav.js")
.script("/static/core/js/misc.js")
.module("/static/core/js/modules/localization.js")

Check warning on line 46 in pointercrate-core-pages/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

pointercrate-core-pages/src/lib.rs#L46

Added line #L46 was not covered by tests
.stylesheet("/static/core/css/icon.css")
.stylesheet("/static/core/css/nav.css")
.stylesheet("/static/core/css/main.css")
Expand Down
Loading
Loading