use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
use crate::core::theme::all::DEFAULT_THEME;
use crate::core::theme::{ChildrenInRegions, DefaultRegion, RegionRef, TemplateRef, ThemeRef};
use crate::core::TypeInfo;
use crate::html::{html, Markup, RoutePath};
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
use crate::locale::L10n;
use crate::locale::{LangId, LanguageIdentifier, RequestLocale};
use crate::service::HttpRequest;
use crate::{builder_fn, util, CowStr};
use std::any::Any;
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt;
pub enum AssetsOp {
SetFavicon(Option<Favicon>),
SetFaviconIfNone(Favicon),
AddStyleSheet(StyleSheet),
RemoveStyleSheet(&'static str),
AddJavaScript(JavaScript),
RemoveJavaScript(&'static str),
}
#[derive(Debug)]
pub enum ContextError {
ParamNotFound,
ParamTypeMismatch {
key: &'static str,
expected: &'static str,
saved: &'static str,
},
}
impl fmt::Display for ContextError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContextError::ParamNotFound => {
write!(f, "parameter not found")
}
ContextError::ParamTypeMismatch {
key,
expected,
saved,
} => write!(
f,
"type mismatch for parameter \"{key}\": expected \"{expected}\", found \"{saved}\""
),
}
}
}
impl std::error::Error for ContextError {}
pub trait Contextual: LangId {
#[builder_fn]
fn with_langid(self, language: &impl LangId) -> Self;
#[builder_fn]
fn with_request(self, request: Option<HttpRequest>) -> Self;
#[builder_fn]
fn with_theme(self, theme: ThemeRef) -> Self;
#[builder_fn]
fn with_template(self, template: TemplateRef) -> Self;
#[builder_fn]
fn with_param<T: 'static>(self, key: &'static str, value: T) -> Self;
#[builder_fn]
fn with_assets(self, op: AssetsOp) -> Self;
#[builder_fn]
fn with_child(self, op: impl Into<ChildOp>) -> Self;
#[builder_fn]
fn with_child_in(self, region_ref: RegionRef, op: impl Into<ChildOp>) -> Self;
fn request(&self) -> Option<&HttpRequest>;
fn theme(&self) -> ThemeRef;
fn template(&self) -> TemplateRef;
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError>;
fn param_or_default<T: Clone + Default + 'static>(&self, key: &'static str) -> T {
self.param::<T>(key).ok().cloned().unwrap_or_default()
}
fn param_or<T: Clone + 'static>(&self, key: &'static str, default: T) -> T {
self.param::<T>(key).ok().cloned().unwrap_or(default)
}
fn param_or_else<T: Clone + 'static, F: FnOnce() -> T>(&self, key: &'static str, f: F) -> T {
self.param::<T>(key).ok().cloned().unwrap_or_else(f)
}
fn favicon(&self) -> Option<&Favicon>;
fn stylesheets(&self) -> &Assets<StyleSheet>;
fn javascripts(&self) -> &Assets<JavaScript>;
fn remove_param(&mut self, key: &'static str) -> bool;
}
#[rustfmt::skip]
pub struct Context {
request : Option<HttpRequest>, locale : RequestLocale, theme : ThemeRef, template : TemplateRef, favicon : Option<Favicon>, stylesheets: Assets<StyleSheet>, javascripts: Assets<JavaScript>, regions : ChildrenInRegions, params : HashMap<&'static str, (Box<dyn Any>, &'static str)>, id_counter : Cell<usize>, messages : Vec<StatusMessage>, }
impl Default for Context {
fn default() -> Self {
Context::new(None)
}
}
impl Context {
#[rustfmt::skip]
pub fn new(request: Option<HttpRequest>) -> Self {
let locale = RequestLocale::from_request(request.as_ref());
Context {
request,
locale,
theme : *DEFAULT_THEME,
template : DEFAULT_THEME.default_template(),
favicon : None,
stylesheets: Assets::<StyleSheet>::new(),
javascripts: Assets::<JavaScript>::new(),
regions : ChildrenInRegions::default(),
params : HashMap::default(),
id_counter : Cell::new(0),
messages : Vec::new(),
}
}
pub fn render_assets(&mut self) -> Markup {
use std::mem::take as mem_take;
let favicon = mem_take(&mut self.favicon); let stylesheets = mem_take(&mut self.stylesheets); let javascripts = mem_take(&mut self.javascripts);
let markup = html! {
@if let Some(fi) = &favicon {
(fi.render(self))
}
(stylesheets.render(self))
(javascripts.render(self))
};
self.favicon = favicon;
self.stylesheets = stylesheets;
self.javascripts = javascripts;
markup
}
pub fn render_region(&mut self, region_ref: RegionRef) -> Markup {
self.regions
.children_for(self.theme, region_ref)
.render(self)
}
pub fn route(&self, path: impl Into<CowStr>) -> RoutePath {
let mut route = RoutePath::new(path);
if self.locale.needs_lang_query() {
route.alter_param("lang", self.locale.langid().to_string());
}
route
}
pub fn required_id<C: Component>(&self, id: Option<String>, parts: usize) -> String {
if let Some(id) = id {
return id;
}
let segments: Vec<&str> = TypeInfo::FullName.of::<C>().split("::").collect();
let parts = if parts == 0 || parts >= segments.len() {
segments.len()
} else {
parts
};
self.id_counter.set(self.id_counter.get() + 1);
let prefix = segments[segments.len() - parts..].join("-").to_lowercase();
util::join!(prefix, "-", self.id_counter.get().to_string())
}
pub fn push_message(&mut self, level: MessageLevel, text: L10n) {
self.messages.push(StatusMessage::new(level, text));
}
pub fn messages(&self) -> &[StatusMessage] {
&self.messages
}
pub fn has_messages(&self) -> bool {
!self.messages.is_empty()
}
}
impl LangId for Context {
#[inline]
fn langid(&self) -> &'static LanguageIdentifier {
self.locale.langid()
}
}
impl Contextual for Context {
#[builder_fn]
fn with_request(mut self, request: Option<HttpRequest>) -> Self {
self.request = request;
self.locale = RequestLocale::from_request(self.request.as_ref());
self
}
#[builder_fn]
fn with_langid(mut self, language: &impl LangId) -> Self {
self.locale.with_langid(language);
self
}
#[builder_fn]
fn with_theme(mut self, theme: ThemeRef) -> Self {
self.theme = theme;
self
}
#[builder_fn]
fn with_template(mut self, template: TemplateRef) -> Self {
self.template = template;
self
}
#[builder_fn]
fn with_param<T: 'static>(mut self, key: &'static str, value: T) -> Self {
let type_name = TypeInfo::FullName.of::<T>();
self.params.insert(key, (Box::new(value), type_name));
self
}
#[builder_fn]
fn with_assets(mut self, op: AssetsOp) -> Self {
match op {
AssetsOp::SetFavicon(favicon) => {
self.favicon = favicon;
}
AssetsOp::SetFaviconIfNone(icon) => {
if self.favicon.is_none() {
self.favicon = Some(icon);
}
}
AssetsOp::AddStyleSheet(css) => {
self.stylesheets.add(css);
}
AssetsOp::RemoveStyleSheet(path) => {
self.stylesheets.remove(path);
}
AssetsOp::AddJavaScript(js) => {
self.javascripts.add(js);
}
AssetsOp::RemoveJavaScript(path) => {
self.javascripts.remove(path);
}
}
self
}
#[builder_fn]
fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
self.regions
.alter_child_in(&DefaultRegion::Content, op.into());
self
}
#[builder_fn]
fn with_child_in(mut self, region_ref: RegionRef, op: impl Into<ChildOp>) -> Self {
self.regions.alter_child_in(region_ref, op.into());
self
}
fn request(&self) -> Option<&HttpRequest> {
self.request.as_ref()
}
fn theme(&self) -> ThemeRef {
self.theme
}
fn template(&self) -> TemplateRef {
self.template
}
fn param<T: 'static>(&self, key: &'static str) -> Result<&T, ContextError> {
let (any, type_name) = self.params.get(key).ok_or(ContextError::ParamNotFound)?;
any.downcast_ref::<T>()
.ok_or_else(|| ContextError::ParamTypeMismatch {
key,
expected: TypeInfo::FullName.of::<T>(),
saved: type_name,
})
}
fn favicon(&self) -> Option<&Favicon> {
self.favicon.as_ref()
}
fn stylesheets(&self) -> &Assets<StyleSheet> {
&self.stylesheets
}
fn javascripts(&self) -> &Assets<JavaScript> {
&self.javascripts
}
fn remove_param(&mut self, key: &'static str) -> bool {
self.params.remove(key).is_some()
}
}