♻️ Major code restructuring

This commit is contained in:
Manuel Cillero 2024-02-09 14:05:38 +01:00
parent a96e203bb3
commit fa66d628a0
221 changed files with 228 additions and 315 deletions

142
src/app.rs Normal file
View file

@ -0,0 +1,142 @@
//! Prepare and run an application created with **Pagetop**.
mod figfont;
use crate::core::{package, package::PackageRef};
use crate::html::Markup;
use crate::response::page::{ErrorPage, ResultPage};
use crate::{config, locale, service, trace, LazyStatic};
#[cfg(feature = "database")]
use crate::db;
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
use actix_session::storage::CookieSessionStore;
use actix_session::SessionMiddleware;
use substring::Substring;
use std::io::Error;
pub struct Application;
impl Application {
pub fn prepare(app: PackageRef) -> Result<Self, Error> {
// On startup.
show_banner();
// Inicia registro de trazas y eventos.
LazyStatic::force(&trace::TRACING);
// Valida el identificador global de idioma.
LazyStatic::force(&locale::LANGID);
#[cfg(feature = "database")]
// Conecta con la base de datos.
LazyStatic::force(&db::DBCONN);
// Registra los paquetes de la aplicación.
package::all::register_packages(app);
// Registra acciones de los paquetes.
package::all::register_actions();
// Inicializa los paquetes.
package::all::init_packages();
#[cfg(feature = "database")]
// Ejecuta actualizaciones pendientes de la base de datos.
package::all::run_migrations();
Ok(Self)
}
pub fn run(self) -> Result<service::Server, Error> {
// Generate cookie key.
let secret_key = service::cookie::Key::generate();
// Prepara el servidor web.
Ok(service::HttpServer::new(move || {
service_app()
.wrap(tracing_actix_web::TracingLogger::default())
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
.session_lifecycle(match config::SETTINGS.server.session_lifetime {
0 => SessionLifecycle::BrowserSession(BrowserSession::default()),
_ => SessionLifecycle::PersistentSession(
PersistentSession::default().session_ttl(
service::cookie::time::Duration::seconds(
config::SETTINGS.server.session_lifetime,
),
),
),
})
.build(),
)
})
.bind(format!(
"{}:{}",
&config::SETTINGS.server.bind_address,
&config::SETTINGS.server.bind_port
))?
.run())
}
pub fn test(
self,
) -> service::App<
impl service::Factory<
service::Request,
Config = (),
Response = service::Response<service::BoxBody>,
Error = service::Error,
InitError = (),
>,
> {
service_app()
}
}
fn service_app() -> service::App<
impl service::Factory<
service::Request,
Config = (),
Response = service::Response<service::BoxBody>,
Error = service::Error,
InitError = (),
>,
> {
service::App::new()
.configure(package::all::configure_services)
.default_service(service::web::route().to(service_not_found))
}
async fn service_not_found(request: service::HttpRequest) -> ResultPage<Markup, ErrorPage> {
Err(ErrorPage::NotFound(request))
}
fn show_banner() {
if config::SETTINGS.app.startup_banner.to_lowercase() != "off" {
// Application name.
let mut app_name = config::SETTINGS.app.name.to_string();
if let Some((term_width, _)) = term_size::dimensions() {
if term_width >= 80 {
let maxlen = (term_width / 10) - 2;
let mut app = app_name.substring(0, maxlen).to_owned();
if app_name.len() > maxlen {
app = format!("{}...", app);
}
app_name = figfont::FIGFONT.convert(&app).unwrap().to_string();
}
}
println!("\n{}", app_name);
// Application description.
if !config::SETTINGS.app.description.is_empty() {
println!("{}\n", config::SETTINGS.app.description);
};
// PageTop version.
println!("Powered by PageTop {}\n", env!("CARGO_PKG_VERSION"));
}
}

28
src/app/figfont.rs Normal file
View file

@ -0,0 +1,28 @@
use crate::{config, LazyStatic};
use figlet_rs::FIGfont;
pub static FIGFONT: LazyStatic<FIGfont> = LazyStatic::new(|| {
let slant = include_str!("slant.flf");
let small = include_str!("small.flf");
let speed = include_str!("speed.flf");
let starwars = include_str!("starwars.flf");
FIGfont::from_content(
match config::SETTINGS.app.startup_banner.to_lowercase().as_str() {
"off" => slant,
"slant" => slant,
"small" => small,
"speed" => speed,
"starwars" => starwars,
_ => {
println!(
"\n FIGfont \"{}\" not found for banner. Using \"Slant\". Check settings files.",
config::SETTINGS.app.startup_banner,
);
slant
}
},
)
.unwrap()
});

1295
src/app/slant.flf Normal file

File diff suppressed because it is too large Load diff

1097
src/app/small.flf Normal file

File diff suppressed because it is too large Load diff

1301
src/app/speed.flf Normal file

File diff suppressed because it is too large Load diff

719
src/app/starwars.flf Normal file
View file

@ -0,0 +1,719 @@
flf2a$ 7 6 22 15 4
starwars.flf by Ryan Youck (youck@cs.uregina.ca) Dec 25/1994
I am not responsible for use of this font
Based on Big.flf by Glenn Chappell
$ $@
$ $@
$ $@
$ $@
$ $@
$ $@
$ $@@
__ $@
| |$@
| |$@
| |$@
|__|$@
(__)$@
$@@
_ _ @
( | )@
V V @
$ @
$ @
$ @
@@
_ _ @
_| || |_$@
|_ __ _|@
_| || |_ @
|_ __ _|@
|_||_| $@
@@
__,--,_.@
/ |@
| (----`@
\ \ $@
.----) | $@
|_ __/ $@
'--' $@@
_ ___$ @
/ \ / /$ @
( o ) / / $ @
\_/ / / _$ @
/ / / \ @
/ / ( o )@
/__/ \_/ @@
@
___ @
( _ ) $@
/ _ \/\@
| (_> <@
\___/\/@
$@@
__ @
(_ )@
|/ @
$ @
$ @
$ @
@@
___@
/ /@
| |$@
| |$@
| |$@
| |$@
\__\@@
___ @
\ \ @
| |@
| |@
| |@
| |@
/__/ @@
_ @
/\| |/\ @
\ ` ' /$@
|_ _|@
/ , . \$@
\/|_|\/ @
@@
@
_ @
_| |_$@
|_ _|@
|_| $@
$ @
@@
@
@
$ @
$ @
__ @
(_ )@
|/ @@
@
@
______ @
|______|@
$ @
$ @
@@
@
@
@
$ @
__ @
(__)@
@@
___@
/ /@
/ / @
/ /$ @
/ /$ @
/__/$ @
@@
___ $@
/ _ \ $@
| | | |$@
| | | |$@
| |_| |$@
\___/ $@
$@@
__ $@
/_ |$@
| |$@
| |$@
| |$@
|_|$@
$@@
___ $@
|__ \ $@
$) |$@
/ / $@
/ /_ $@
|____|$@
$@@
____ $@
|___ \ $@
__) |$@
|__ < $@
___) |$@
|____/ $@
$@@
_ _ $@
| || | $@
| || |_ $@
|__ _|$@
| | $@
|_| $@
$@@
_____ $@
| ____|$@
| |__ $@
|___ \ $@
___) |$@
|____/ $@
$@@
__ $@
/ / $@
/ /_ $@
| '_ \ $@
| (_) |$@
\___/ $@
$@@
______ $@
|____ |$@
$/ / $@
/ / $@
/ / $@
/_/ $@
$@@
___ $@
/ _ \ $@
| (_) |$@
> _ < $@
| (_) |$@
\___/ $@
$@@
___ $@
/ _ \ $@
| (_) |$@
\__, |$@
/ / $@
/_/ $@
$@@
@
_ @
(_)@
$ @
_ @
(_)@
@@
@
_ @
(_)@
$ @
_ @
( )@
|/ @@
___@
/ /@
/ /$@
< <$ @
\ \$@
\__\@
@@
@
______ @
|______|@
______ @
|______|@
@
@@
___ @
\ \$ @
\ \ @
> >@
/ / @
/__/$ @
@@
______ $@
| \ $@
`----) |$@
/ / $@
|__| $@
__ $@
(__) $@@
____ @
/ __ \ @
/ / _` |@
| | (_| |@
\ \__,_|@
\____/ @
@@
___ $ @
/ \ $ @
/ ^ \$ @
/ /_\ \$ @
/ _____ \$ @
/__/ \__\$@
$@@
.______ $@
| _ \ $@
| |_) |$@
| _ < $@
| |_) |$@
|______/ $@
$@@
______$@
/ |@
| ,----'@
| | $@
| `----.@
\______|@
$@@
_______ $@
| \$@
| .--. |@
| | | |@
| '--' |@
|_______/$@
$@@
_______ @
| ____|@
| |__ $@
| __| $@
| |____ @
|_______|@
@@
_______ @
| ____|@
| |__ $@
| __| $@
| | $ @
|__| @
@@
_______ @
/ _____|@
| | __ $@
| | |_ |$@
| |__| |$@
\______|$@
$@@
__ __ $@
| | | |$@
| |__| |$@
| __ |$@
| | | |$@
|__| |__|$@
$@@
__ $@
| |$@
| |$@
| |$@
| |$@
|__|$@
$@@
__ $@
| |$@
| |$@
.--. | |$@
| `--' |$@
\______/ $@
$@@
__ ___$@
| |/ /$@
| ' / $@
| < $@
| . \ $@
|__|\__\$@
$@@
__ $@
| | $@
| | $@
| | $@
| `----.@
|_______|@
$@@
.___ ___.$@
| \/ |$@
| \ / |$@
| |\/| |$@
| | | |$@
|__| |__|$@
$@@
.__ __.$@
| \ | |$@
| \| |$@
| . ` |$@
| |\ |$@
|__| \__|$@
$@@
______ $@
/ __ \ $@
| | | |$@
| | | |$@
| `--' |$@
\______/ $@
$@@
.______ $@
| _ \ $@
| |_) |$@
| ___/ $@
| | $ @
| _| $ @
$ @@
______ $ @
/ __ \ $ @
| | | | $ @
| | | | $ @
| `--' '--. @
\_____\_____\@
$ @@
.______ $ @
| _ \ $ @
| |_) | $ @
| / $ @
| |\ \----.@
| _| `._____|@
$@@
_______.@
/ |@
| (----`@
\ \ $@
.----) | $@
|_______/ $@
$@@
.___________.@
| |@
`---| |----`@
| | $ @
| | $ @
|__| $ @
$ @@
__ __ $@
| | | |$@
| | | |$@
| | | |$@
| `--' |$@
\______/ $@
$@@
____ ____$@
\ \ / /$@
\ \/ /$ @
\ /$ @
\ /$ @
\__/$ @
$ @@
____ __ ____$@
\ \ / \ / /$@
\ \/ \/ /$ @
\ /$ @
\ /\ /$ @
\__/ \__/$ @
$ @@
___ ___$@
\ \ / /$@
\ V / $@
> < $@
/ . \ $@
/__/ \__\$@
$@@
____ ____$@
\ \ / /$@
\ \/ /$ @
\_ _/$ @
| |$ @
|__|$ @
$ @@
________ $@
| / $@
`---/ / $@
/ / $@
/ /----.@
/________|@
$@@
____ @
| |@
| |-`@
| | $@
| | $@
| |-.@
|____|@@
___ @
\ \ $ @
\ \$ @
\ \$ @
\ \$@
\__\@
@@
____ @
| |@
`-| |@
| |@
| |@
.-| |@
|____|@@
___ @
/ \ @
/--^--\@
$@
$@
$@
$@@
@
@
@
$ @
$ @
______ @
|______|@@
__ @
( _)@
\| @
$ @
$ @
$ @
@@
___ $ @
/ \ $ @
/ ^ \$ @
/ /_\ \$ @
/ _____ \$ @
/__/ \__\$@
$@@
.______ $@
| _ \ $@
| |_) |$@
| _ < $@
| |_) |$@
|______/ $@
$@@
______$@
/ |@
| ,----'@
| | $@
| `----.@
\______|@
$@@
_______ $@
| \$@
| .--. |@
| | | |@
| '--' |@
|_______/$@
$@@
_______ @
| ____|@
| |__ $@
| __| $@
| |____ @
|_______|@
@@
_______ @
| ____|@
| |__ $@
| __| $@
| | $ @
|__| @
@@
_______ @
/ _____|@
| | __ $@
| | |_ |$@
| |__| |$@
\______|$@
$@@
__ __ $@
| | | |$@
| |__| |$@
| __ |$@
| | | |$@
|__| |__|$@
$@@
__ $@
| |$@
| |$@
| |$@
| |$@
|__|$@
$@@
__ $@
| |$@
| |$@
.--. | |$@
| `--' |$@
\______/ $@
$@@
__ ___$@
| |/ /$@
| ' / $@
| < $@
| . \ $@
|__|\__\$@
$@@
__ $@
| | $@
| | $@
| | $@
| `----.@
|_______|@
$@@
.___ ___.$@
| \/ |$@
| \ / |$@
| |\/| |$@
| | | |$@
|__| |__|$@
$@@
.__ __.$@
| \ | |$@
| \| |$@
| . ` |$@
| |\ |$@
|__| \__|$@
$@@
______ $@
/ __ \ $@
| | | |$@
| | | |$@
| `--' |$@
\______/ $@
$@@
.______ $@
| _ \ $@
| |_) |$@
| ___/ $@
| | $ @
| _| $ @
$ @@
______ $ @
/ __ \ $ @
| | | | $ @
| | | | $ @
| `--' '--. @
\_____\_____\@
$ @@
.______ $ @
| _ \ $ @
| |_) | $ @
| / $ @
| |\ \----.@
| _| `._____|@
$@@
_______.@
/ |@
| (----`@
\ \ $@
.----) | $@
|_______/ $@
$@@
.___________.@
| |@
`---| |----`@
| | $ @
| | $ @
|__| $ @
$ @@
__ __ $@
| | | |$@
| | | |$@
| | | |$@
| `--' |$@
\______/ $@
$@@
____ ____$@
\ \ / /$@
\ \/ /$ @
\ /$ @
\ /$ @
\__/$ @
$ @@
____ __ ____$@
\ \ / \ / /$@
\ \/ \/ /$ @
\ /$ @
\ /\ /$ @
\__/ \__/$ @
$ @@
___ ___$@
\ \ / /$@
\ V / $@
> < $@
/ . \ $@
/__/ \__\$@
$@@
____ ____$@
\ \ / /$@
\ \/ /$ @
\_ _/$ @
| |$ @
|__|$ @
$ @@
________ $@
| / $@
`---/ / $@
/ / $@
/ /----.@
/________|@
$@@
___@
/ /@
| |$@
/ /$ @
\ \$ @
| |$@
\__\@@
__ $@
| |$@
| |$@
| |$@
| |$@
| |$@
|__|$@@
___ @
\ \$ @
| | @
\ \@
/ /@
| | @
/__/$ @@
__ _ @
/ \/ |@
|_/\__/ @
$ @
$ @
$ @
@@
_ _ @
(_)_(_) @
/ \ @
/ _ \ @
/ ___ \ @
/_/ \_\@
@@
_ _ @
(_)_(_)@
/ _ \ @
| | | |@
| |_| |@
\___/ @
@@
_ _ @
(_) (_)@
| | | |@
| | | |@
| |_| |@
\___/ @
@@
_ _ @
(_) (_)@
__ _ @
/ _` |@
| (_| |@
\__,_|@
@@
_ _ @
(_) (_)@
___ @
/ _ \ @
| (_) |@
\___/ @
@@
_ _ @
(_) (_)@
_ _ @
| | | |@
| |_| |@
\__,_|@
@@
___ @
/ _ \ @
| | ) |@
| |< < @
| | ) |@
| ||_/ @
|_| @@

7
src/base.rs Normal file
View file

@ -0,0 +1,7 @@
//! Base actions, components, packages, and themes.
pub mod action;
pub mod component;
pub mod theme;

3
src/base/action.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod component;
pub mod page;

View file

@ -0,0 +1,9 @@
use crate::prelude::*;
pub type FnActionComponent<C> = fn(component: &mut C, cx: &mut Context);
mod before_prepare_component;
pub use before_prepare_component::*;
mod after_prepare_component;
pub use after_prepare_component::*;

View file

@ -0,0 +1,55 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::FnActionComponent;
#[derive(BaseHandle)]
pub struct AfterPrepareComponent<C: ComponentTrait> {
f: FnActionComponent<C>,
referer_handle: Option<Handle>,
referer_id: OptionId,
weight: Weight,
}
impl<C: ComponentTrait> ActionTrait for AfterPrepareComponent<C> {
fn referer_handle(&self) -> Option<Handle> {
self.referer_handle
}
fn referer_id(&self) -> Option<String> {
self.referer_id.get()
}
fn weight(&self) -> Weight {
self.weight
}
}
impl<C: ComponentTrait> AfterPrepareComponent<C> {
pub fn new(f: FnActionComponent<C>) -> Self {
AfterPrepareComponent {
f,
referer_handle: Some(C::static_handle()),
referer_id: OptionId::default(),
weight: 0,
}
}
pub fn filter_by_referer_id(mut self, id: impl Into<String>) -> Self {
self.referer_id.alter_value(id);
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[inline(always)]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context, referer_id: Option<String>) {
dispatch_actions(
(Self::static_handle(), Some(component.handle()), referer_id),
|action| (action_ref::<AfterPrepareComponent<C>>(&**action).f)(component, cx),
);
}
}

View file

@ -0,0 +1,55 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::FnActionComponent;
#[derive(BaseHandle)]
pub struct BeforePrepareComponent<C: ComponentTrait> {
f: FnActionComponent<C>,
referer_handle: Option<Handle>,
referer_id: OptionId,
weight: Weight,
}
impl<C: ComponentTrait> ActionTrait for BeforePrepareComponent<C> {
fn referer_handle(&self) -> Option<Handle> {
self.referer_handle
}
fn referer_id(&self) -> Option<String> {
self.referer_id.get()
}
fn weight(&self) -> Weight {
self.weight
}
}
impl<C: ComponentTrait> BeforePrepareComponent<C> {
pub fn new(f: FnActionComponent<C>) -> Self {
BeforePrepareComponent {
f,
referer_handle: Some(C::static_handle()),
referer_id: OptionId::default(),
weight: 0,
}
}
pub fn filter_by_referer_id(mut self, id: impl Into<String>) -> Self {
self.referer_id.alter_value(id);
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[inline(always)]
pub(crate) fn dispatch(component: &mut C, cx: &mut Context, referer_id: Option<String>) {
dispatch_actions(
(Self::static_handle(), Some(component.handle()), referer_id),
|action| (action_ref::<BeforePrepareComponent<C>>(&**action).f)(component, cx),
);
}
}

9
src/base/action/page.rs Normal file
View file

@ -0,0 +1,9 @@
use crate::prelude::*;
pub type FnActionPage = fn(page: &mut Page);
mod before_prepare_body;
pub use before_prepare_body::*;
mod after_prepare_body;
pub use after_prepare_body::*;

View file

@ -0,0 +1,34 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::FnActionPage;
#[derive(BaseHandle)]
pub struct AfterPrepareBody {
f: FnActionPage,
weight: Weight,
}
impl ActionTrait for AfterPrepareBody {
fn weight(&self) -> Weight {
self.weight
}
}
impl AfterPrepareBody {
pub fn new(f: FnActionPage) -> Self {
AfterPrepareBody { f, weight: 0 }
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[inline(always)]
pub(crate) fn dispatch(page: &mut Page) {
dispatch_actions((Self::static_handle(), None, None), |action| {
(action_ref::<AfterPrepareBody>(&**action).f)(page)
});
}
}

View file

@ -0,0 +1,34 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::FnActionPage;
#[derive(BaseHandle)]
pub struct BeforePrepareBody {
f: FnActionPage,
weight: Weight,
}
impl ActionTrait for BeforePrepareBody {
fn weight(&self) -> Weight {
self.weight
}
}
impl BeforePrepareBody {
pub fn new(f: FnActionPage) -> Self {
BeforePrepareBody { f, weight: 0 }
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[inline(always)]
pub(crate) fn dispatch(page: &mut Page) {
dispatch_actions((Self::static_handle(), None, None), |action| {
(action_ref::<BeforePrepareBody>(&**action).f)(page)
});
}
}

202
src/base/component.rs Normal file
View file

@ -0,0 +1,202 @@
use crate::core::component::{Context, ContextOp};
use crate::html::{JavaScript, StyleSheet};
use crate::{SmartDefault, Weight};
// Context parameters.
pub const PARAM_BASE_WEIGHT: &str = "base.weight";
pub const PARAM_BASE_INCLUDE_ICONS: &str = "base.include.icon";
pub const PARAM_BASE_INCLUDE_FLEX_ASSETS: &str = "base.include.flex";
pub const PARAM_BASE_INCLUDE_MENU_ASSETS: &str = "base.include.menu";
pub(crate) fn add_base_assets(cx: &mut Context) {
let weight = cx.get_param::<Weight>(PARAM_BASE_WEIGHT).unwrap_or(-90);
cx.alter(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/root.css")
.with_version("0.0.1")
.with_weight(weight),
));
if let Some(true) = cx.get_param::<bool>(PARAM_BASE_INCLUDE_ICONS) {
cx.alter(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/icons.min.css")
.with_version("1.11.1")
.with_weight(weight),
));
}
if let Some(true) = cx.get_param::<bool>(PARAM_BASE_INCLUDE_FLEX_ASSETS) {
cx.alter(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/flex.css")
.with_version("0.0.1")
.with_weight(weight),
));
}
if let Some(true) = cx.get_param::<bool>(PARAM_BASE_INCLUDE_MENU_ASSETS) {
cx.alter(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/menu.css")
.with_version("0.0.1")
.with_weight(weight),
))
.alter(ContextOp::AddJavaScript(
JavaScript::at("/base/js/menu.js")
.with_version("0.0.1")
.with_weight(weight),
));
}
cx.alter(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/looks.css")
.with_version("0.0.1")
.with_weight(weight),
))
.alter(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/buttons.css")
.with_version("0.0.1")
.with_weight(weight),
));
}
// *************************************************************************************************
#[rustfmt::skip]
#[derive(SmartDefault)]
pub enum BreakPoint {
#[default]
None, /* Does not apply. Rest initially assume 1 pixel = 0.0625em */
SM, /* PageTop default applies to <= 568px - @media screen and (max-width: 35.5em) */
MD, /* PageTop default applies to <= 768px - @media screen and (max-width: 48em) */
LG, /* PageTop default applies to <= 992px - @media screen and (max-width: 62em) */
XL, /* PageTop default applies to <= 1280px - @media screen and (max-width: 80em) */
X2L, /* PageTop default applies to <= 1440px - @media screen and (max-width: 90em) */
X3L, /* PageTop default applies to <= 1920px - @media screen and (max-width: 120em) */
X2K, /* PageTop default applies to <= 2560px - @media screen and (max-width: 160em) */
}
#[rustfmt::skip]
impl ToString for BreakPoint {
fn to_string(&self) -> String {
String::from(match self {
BreakPoint::None => "pt-bp__none",
BreakPoint::SM => "pt-bp__sm",
BreakPoint::MD => "pt-bp__md",
BreakPoint::LG => "pt-bp__lg",
BreakPoint::XL => "pt-bp__xl",
BreakPoint::X2L => "pt-bp__x2l",
BreakPoint::X3L => "pt-bp__x3l",
BreakPoint::X2K => "pt-bp__x2k",
})
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum ButtonStyle {
#[default]
Default,
Info,
Success,
Warning,
Danger,
Light,
Dark,
Link,
}
#[rustfmt::skip]
impl ToString for ButtonStyle {
fn to_string(&self) -> String {
String::from(match self {
ButtonStyle::Default => "pt-button__default",
ButtonStyle::Info => "pt-button__info",
ButtonStyle::Success => "pt-button__success",
ButtonStyle::Warning => "pt-button__warning",
ButtonStyle::Danger => "pt-button__danger",
ButtonStyle::Light => "pt-button__light",
ButtonStyle::Dark => "pt-button__dark",
ButtonStyle::Link => "pt-button__link",
})
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum FontSize {
ExtraLarge,
XxLarge,
XLarge,
Large,
Medium,
#[default]
Normal,
Small,
XSmall,
XxSmall,
ExtraSmall,
}
#[rustfmt::skip]
impl ToString for FontSize {
fn to_string(&self) -> String {
String::from(match self {
FontSize::ExtraLarge => "pt-fs__x3l",
FontSize::XxLarge => "pt-fs__x2l",
FontSize::XLarge => "pt-fs__xl",
FontSize::Large => "pt-fs__l",
FontSize::Medium => "pt-fs__m",
FontSize::Normal => "",
FontSize::Small => "pt-fs__s",
FontSize::XSmall => "pt-fs__xs",
FontSize::XxSmall => "pt-fs__x2s",
FontSize::ExtraSmall => "pt-fs__x3s",
})
}
}
// *************************************************************************************************
mod html;
pub use html::Html;
mod translate;
pub use translate::Translate;
mod wrapper;
pub use wrapper::{Wrapper, WrapperType};
pub mod flex;
mod icon;
pub use icon::Icon;
mod heading;
pub use heading::{Heading, HeadingSize, HeadingType};
mod paragraph;
pub use paragraph::Paragraph;
mod button;
pub use button::{Button, ButtonTarget};
mod image;
pub use image::{Image, ImageSize};
mod block;
pub use block::Block;
mod branding;
pub use branding::Branding;
mod powered_by;
pub use powered_by::{PoweredBy, PoweredByLogo};
mod error403;
pub use error403::Error403;
mod error404;
pub use error404::Error404;
pub mod menu;
pub use menu::Menu;
pub mod form;
pub use form::{Form, FormMethod};

101
src/base/component/block.rs Normal file
View file

@ -0,0 +1,101 @@
use crate::prelude::*;
use crate::BaseHandle;
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Block {
id : OptionId,
weight : Weight,
renderable: Renderable,
classes : OptionClasses,
title : OptionTranslated,
stuff : AnyComponents,
}
impl ComponentTrait for Block {
fn new() -> Self {
Block::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.prepend_classes("pt-block");
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let block_body = self.components().render(cx);
if !block_body.is_empty() {
let id = cx.required_id::<Block>(self.id());
return PrepareMarkup::With(html! {
div id=(id) class=[self.classes().get()] {
@if let Some(title) = self.title().using(cx.langid()) {
h2 class="pt-block__title" { (title) }
}
div class="pt-block__body" { (block_body) }
}
});
}
PrepareMarkup::None
}
}
impl Block {
// Block BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_title(&mut self, title: L10n) -> &mut Self {
self.title.alter_value(title);
self
}
#[rustfmt::skip]
pub fn add_component(mut self, component: impl ComponentTrait) -> Self {
self.stuff.alter_value(ArcAnyOp::Add(ArcAnyComponent::new(component)));
self
}
#[fn_with]
pub fn alter_components(&mut self, op: ArcAnyOp) -> &mut Self {
self.stuff.alter_value(op);
self
}
// Block GETTERS.
pub fn title(&self) -> &OptionTranslated {
&self.title
}
pub fn components(&self) -> &AnyComponents {
&self.stuff
}
}

View file

@ -0,0 +1,124 @@
use crate::prelude::*;
use crate::BaseHandle;
#[rustfmt::skip]
#[derive(BaseHandle, SmartDefault)]
pub struct Branding {
id : OptionId,
weight : Weight,
renderable: Renderable,
#[default(_code = "config::SETTINGS.app.name.to_owned()")]
app_name : String,
slogan : OptionTranslated,
logo : OptionComponent<Image>,
#[default(_code = "|_| \"/\"")]
frontpage : FnContextualPath,
}
impl ComponentTrait for Branding {
fn new() -> Self {
Branding::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let logo = self.logo().render(cx);
let title = L10n::l("site_home").using(cx.langid());
PrepareMarkup::With(html! {
div id=[self.id()] class="pt-branding" {
div class="pt-branding__wrapper" {
@if !logo.is_empty() {
div class="pt-branding__logo" { (logo) }
}
div class="pt-branding__text" {
div class="pt-branding__name" {
a href=(self.frontpage()(cx)) title=[title] rel="home" {
(self.app_name())
}
}
@if let Some(slogan) = self.slogan().using(cx.langid()) {
div class="pt-branding__slogan" {
(slogan)
}
}
}
}
}
})
}
}
impl Branding {
// Branding BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_app_name(&mut self, app_name: impl Into<String>) -> &mut Self {
self.app_name = app_name.into();
self
}
#[fn_with]
pub fn alter_slogan(&mut self, slogan: L10n) -> &mut Self {
self.slogan.alter_value(slogan);
self
}
#[fn_with]
pub fn alter_logo(&mut self, logo: Option<Image>) -> &mut Self {
self.logo.alter_value(logo);
self
}
#[fn_with]
pub fn alter_frontpage(&mut self, frontpage: FnContextualPath) -> &mut Self {
self.frontpage = frontpage;
self
}
// Branding GETTERS.
pub fn app_name(&self) -> &String {
&self.app_name
}
pub fn slogan(&self) -> &OptionTranslated {
&self.slogan
}
pub fn logo(&self) -> &OptionComponent<Image> {
&self.logo
}
pub fn frontpage(&self) -> &FnContextualPath {
&self.frontpage
}
}

View file

@ -0,0 +1,171 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(SmartDefault)]
pub enum ButtonTarget {
#[default]
Default,
Blank,
Parent,
Top,
Context(String),
}
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Button {
id : OptionId,
weight : Weight,
renderable : Renderable,
classes : OptionClasses,
style : ButtonStyle,
font_size : FontSize,
left_icon : OptionComponent<Icon>,
right_icon : OptionComponent<Icon>,
href : OptionString,
html : OptionTranslated,
target : ButtonTarget,
}
impl ComponentTrait for Button {
fn new() -> Self {
Button::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.prepend_classes([self.style().to_string(), self.font_size().to_string()].join(" "));
}
#[rustfmt::skip]
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let target = match &self.target() {
ButtonTarget::Blank => Some("_blank"),
ButtonTarget::Parent => Some("_parent"),
ButtonTarget::Top => Some("_top"),
ButtonTarget::Context(name) => Some(name.as_str()),
_ => None,
};
PrepareMarkup::With(html! {
a
id=[self.id()]
class=[self.classes().get()]
href=[self.href().get()]
target=[target]
{
(self.left_icon().render(cx))
" " span { (self.html().escaped(cx.langid())) } " "
(self.right_icon().render(cx))
}
})
}
}
impl Button {
pub fn anchor(href: impl Into<String>, html: L10n) -> Self {
Button::default().with_href(href).with_html(html)
}
// Button BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_style(&mut self, style: ButtonStyle) -> &mut Self {
self.style = style;
self
}
#[fn_with]
pub fn alter_font_size(&mut self, font_size: FontSize) -> &mut Self {
self.font_size = font_size;
self
}
#[fn_with]
pub fn alter_left_icon(&mut self, icon: Option<Icon>) -> &mut Self {
self.left_icon.alter_value(icon);
self
}
#[fn_with]
pub fn alter_right_icon(&mut self, icon: Option<Icon>) -> &mut Self {
self.right_icon.alter_value(icon);
self
}
#[fn_with]
pub fn alter_href(&mut self, href: impl Into<String>) -> &mut Self {
self.href.alter_value(href);
self
}
#[fn_with]
pub fn alter_html(&mut self, html: L10n) -> &mut Self {
self.html.alter_value(html);
self
}
#[fn_with]
pub fn alter_target(&mut self, target: ButtonTarget) -> &mut Self {
self.target = target;
self
}
// Button GETTERS.
pub fn style(&self) -> &ButtonStyle {
&self.style
}
pub fn font_size(&self) -> &FontSize {
&self.font_size
}
pub fn left_icon(&self) -> &OptionComponent<Icon> {
&self.left_icon
}
pub fn right_icon(&self) -> &OptionComponent<Icon> {
&self.right_icon
}
pub fn href(&self) -> &OptionString {
&self.href
}
pub fn html(&self) -> &OptionTranslated {
&self.html
}
pub fn target(&self) -> &ButtonTarget {
&self.target
}
}

View file

@ -0,0 +1,20 @@
use crate::core::component::{ComponentTrait, Context};
use crate::html::{html, PrepareMarkup};
use crate::BaseHandle;
#[derive(BaseHandle)]
pub struct Error403;
impl ComponentTrait for Error403 {
fn new() -> Self {
Error403
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div {
h1 { ("FORBIDDEN ACCESS") }
}
})
}
}

View file

@ -0,0 +1,20 @@
use crate::core::component::{ComponentTrait, Context};
use crate::html::{html, PrepareMarkup};
use crate::BaseHandle;
#[derive(BaseHandle)]
pub struct Error404;
impl ComponentTrait for Error404 {
fn new() -> Self {
Error404
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div {
h1 { ("RESOURCE NOT FOUND") }
}
})
}
}

322
src/base/component/flex.rs Normal file
View file

@ -0,0 +1,322 @@
mod container;
pub use container::Container;
mod item;
pub use item::Item;
use crate::prelude::*;
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum Direction {
#[default]
Default,
Row(BreakPoint),
RowReverse(BreakPoint),
Column(BreakPoint),
ColumnReverse(BreakPoint),
}
#[rustfmt::skip]
impl ToString for Direction {
fn to_string(&self) -> String {
match self {
Direction::Default => concat_string!(
"pt-flex__container pt-flex__row ", BreakPoint::default().to_string()
),
Direction::Row(breakpoint) => concat_string!(
"pt-flex__container pt-flex__row ", breakpoint.to_string()
),
Direction::RowReverse(breakpoint) => concat_string!(
"pt-flex__container pt-flex__row pt-flex__reverse ", breakpoint.to_string()
),
Direction::Column(breakpoint) => concat_string!(
"pt-flex__container pt-flex__col ", breakpoint.to_string()
),
Direction::ColumnReverse(breakpoint) => concat_string!(
"pt-flex__container pt-flex__col pt-flex__reverse ", breakpoint.to_string()
),
}
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum WrapAlign {
#[default]
Default,
NoWrap,
Wrap(ContentAlign),
WrapReverse(ContentAlign),
}
#[rustfmt::skip]
impl ToString for WrapAlign {
fn to_string(&self) -> String {
match self {
WrapAlign::Default => "".to_owned(),
WrapAlign::NoWrap => "flex-nowrap".to_owned(),
WrapAlign::Wrap(a) => concat_string!("pt-flex__wrap ", a.to_string()),
WrapAlign::WrapReverse(a) => concat_string!("pt-flex__wrap-reverse ", a.to_string()),
}
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum ContentAlign {
#[default]
Default,
Start,
End,
Center,
Stretch,
SpaceBetween,
SpaceAround,
}
#[rustfmt::skip]
impl ToString for ContentAlign {
fn to_string(&self) -> String {
String::from(match self {
ContentAlign::Default => "",
ContentAlign::Start => "pt-flex__align-start",
ContentAlign::End => "pt-flex__align-end",
ContentAlign::Center => "pt-flex__align-center",
ContentAlign::Stretch => "pt-flex__align-stretch",
ContentAlign::SpaceBetween => "pt-flex__align-space-between",
ContentAlign::SpaceAround => "pt-flex__align-space-around",
})
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum ContentJustify {
#[default]
Default,
Start,
End,
Center,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
#[rustfmt::skip]
impl ToString for ContentJustify {
fn to_string(&self) -> String {
String::from(match self {
ContentJustify::Default => "",
ContentJustify::Start => "pt-flex__justify-start",
ContentJustify::End => "pt-flex__justify-end",
ContentJustify::Center => "pt-flex__justify-center",
ContentJustify::SpaceBetween => "pt-flex__justify-space-between",
ContentJustify::SpaceAround => "pt-flex__justify-space-around",
ContentJustify::SpaceEvenly => "pt-flex__justify-space-evenly",
})
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum ItemAlign {
#[default]
Default,
Top,
Bottom,
Middle,
Stretch,
Baseline,
}
#[rustfmt::skip]
impl ToString for ItemAlign {
fn to_string(&self) -> String {
String::from(match self {
ItemAlign::Default => "",
ItemAlign::Top => "pt-flex__item-top",
ItemAlign::Bottom => "pt-flex__item-bottom",
ItemAlign::Middle => "pt-flex__item-middle",
ItemAlign::Stretch => "pt-flex__item-stretch",
ItemAlign::Baseline => "pt-flex__item-baseline",
})
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum Gap {
#[default]
Default,
Row(unit::Value),
Column(unit::Value),
Distinct(unit::Value, unit::Value),
Both(unit::Value),
}
#[rustfmt::skip]
impl ToString for Gap {
fn to_string(&self) -> String {
match self {
Gap::Default => "".to_owned(),
Gap::Row(r) => concat_string!("row-gap: ", r.to_string(), ";"),
Gap::Column(c) => concat_string!("column-gap: ", c.to_string(), ";"),
Gap::Distinct(r, c) => concat_string!("gap: ", r.to_string(), " ", c.to_string(), ";"),
Gap::Both(v) => concat_string!("gap: ", v.to_string(), ";"),
}
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum ItemGrow {
#[default]
Default,
Is1,
Is2,
Is3,
Is4,
Is5,
Is6,
Is7,
Is8,
Is9,
}
#[rustfmt::skip]
impl ToString for ItemGrow {
fn to_string(&self) -> String {
String::from(match self {
ItemGrow::Default => "",
ItemGrow::Is1 => "pt-flex__grow-1",
ItemGrow::Is2 => "pt-flex__grow-2",
ItemGrow::Is3 => "pt-flex__grow-3",
ItemGrow::Is4 => "pt-flex__grow-4",
ItemGrow::Is5 => "pt-flex__grow-5",
ItemGrow::Is6 => "pt-flex__grow-6",
ItemGrow::Is7 => "pt-flex__grow-7",
ItemGrow::Is8 => "pt-flex__grow-8",
ItemGrow::Is9 => "pt-flex__grow-9",
})
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum ItemShrink {
#[default]
Default,
Is1,
Is2,
Is3,
Is4,
Is5,
Is6,
Is7,
Is8,
Is9,
}
#[rustfmt::skip]
impl ToString for ItemShrink {
fn to_string(&self) -> String {
String::from(match self {
ItemShrink::Default => "",
ItemShrink::Is1 => "pt-flex__shrink-1",
ItemShrink::Is2 => "pt-flex__shrink-2",
ItemShrink::Is3 => "pt-flex__shrink-3",
ItemShrink::Is4 => "pt-flex__shrink-4",
ItemShrink::Is5 => "pt-flex__shrink-5",
ItemShrink::Is6 => "pt-flex__shrink-6",
ItemShrink::Is7 => "pt-flex__shrink-7",
ItemShrink::Is8 => "pt-flex__shrink-8",
ItemShrink::Is9 => "pt-flex__shrink-9",
})
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum ItemSize {
#[default]
Default,
Percent10,
Percent20,
Percent25,
Percent33,
Percent40,
Percent50,
Percent60,
Percent66,
Percent75,
Percent80,
Percent90,
}
#[rustfmt::skip]
impl ToString for ItemSize {
fn to_string(&self) -> String {
String::from(match self {
ItemSize::Default => "",
ItemSize::Percent10 => "pt-flex__width-10",
ItemSize::Percent20 => "pt-flex__width-20",
ItemSize::Percent25 => "pt-flex__width-25",
ItemSize::Percent33 => "pt-flex__width-33",
ItemSize::Percent40 => "pt-flex__width-40",
ItemSize::Percent50 => "pt-flex__width-50",
ItemSize::Percent60 => "pt-flex__width-60",
ItemSize::Percent66 => "pt-flex__width-66",
ItemSize::Percent75 => "pt-flex__width-75",
ItemSize::Percent80 => "pt-flex__width-80",
ItemSize::Percent90 => "pt-flex__width-90",
})
}
}
// *************************************************************************************************
#[derive(SmartDefault)]
pub enum ItemOffset {
#[default]
Default,
Offset10,
Offset20,
Offset25,
Offset33,
Offset40,
Offset50,
Offset60,
Offset66,
Offset75,
Offset80,
Offset90,
}
#[rustfmt::skip]
impl ToString for ItemOffset {
fn to_string(&self) -> String {
String::from(match self {
ItemOffset::Default => "",
ItemOffset::Offset10 => "pt-flex__offset-10",
ItemOffset::Offset20 => "pt-flex__offset-20",
ItemOffset::Offset25 => "pt-flex__offset-25",
ItemOffset::Offset33 => "pt-flex__offset-33",
ItemOffset::Offset40 => "pt-flex__offset-40",
ItemOffset::Offset50 => "pt-flex__offset-50",
ItemOffset::Offset60 => "pt-flex__offset-60",
ItemOffset::Offset66 => "pt-flex__offset-66",
ItemOffset::Offset75 => "pt-flex__offset-75",
ItemOffset::Offset80 => "pt-flex__offset-80",
ItemOffset::Offset90 => "pt-flex__offset-90",
})
}
}

View file

@ -0,0 +1,152 @@
use crate::prelude::*;
use crate::BaseHandle;
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Container {
id : OptionId,
weight : Weight,
renderable : Renderable,
classes : OptionClasses,
items : TypedComponents<flex::Item>,
direction : flex::Direction,
wrap_align : flex::WrapAlign,
content_justify: flex::ContentJustify,
items_align : flex::ItemAlign,
gap : flex::Gap,
}
impl ComponentTrait for Container {
fn new() -> Self {
Container::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn setup_before_prepare(&mut self, cx: &mut Context) {
self.prepend_classes(
[
self.direction().to_string(),
self.wrap_align().to_string(),
self.content_justify().to_string(),
self.items_align().to_string(),
]
.join(" "),
);
cx.set_param::<bool>(PARAM_BASE_INCLUDE_FLEX_ASSETS, true);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let gap = match self.gap() {
flex::Gap::Default => None,
_ => Some(self.gap().to_string()),
};
PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] style=[gap] {
(self.items().render(cx))
}
})
}
}
impl Container {
// Container BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[rustfmt::skip]
pub fn add_item(mut self, item: flex::Item) -> Self {
self.items.alter_value(ArcTypedOp::Add(ArcTypedComponent::new(item)));
self
}
#[fn_with]
pub fn alter_items(&mut self, op: ArcTypedOp<flex::Item>) -> &mut Self {
self.items.alter_value(op);
self
}
#[fn_with]
pub fn alter_direction(&mut self, direction: flex::Direction) -> &mut Self {
self.direction = direction;
self
}
#[fn_with]
pub fn alter_wrap_align(&mut self, wrap: flex::WrapAlign) -> &mut Self {
self.wrap_align = wrap;
self
}
#[fn_with]
pub fn alter_content_justify(&mut self, justify: flex::ContentJustify) -> &mut Self {
self.content_justify = justify;
self
}
#[fn_with]
pub fn alter_items_align(&mut self, align: flex::ItemAlign) -> &mut Self {
self.items_align = align;
self
}
#[fn_with]
pub fn alter_gap(&mut self, gap: flex::Gap) -> &mut Self {
self.gap = gap;
self
}
// Container GETTERS.
pub fn items(&self) -> &TypedComponents<flex::Item> {
&self.items
}
pub fn direction(&self) -> &flex::Direction {
&self.direction
}
pub fn wrap_align(&self) -> &flex::WrapAlign {
&self.wrap_align
}
pub fn content_justify(&self) -> &flex::ContentJustify {
&self.content_justify
}
pub fn items_align(&self) -> &flex::ItemAlign {
&self.items_align
}
pub fn gap(&self) -> &flex::Gap {
&self.gap
}
}

View file

@ -0,0 +1,166 @@
use crate::prelude::*;
use crate::BaseHandle;
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Item {
id : OptionId,
weight : Weight,
renderable : Renderable,
classes : OptionClasses,
inner_classes: OptionClasses,
item_grow : flex::ItemGrow,
item_shrink : flex::ItemShrink,
item_size : flex::ItemSize,
item_offset : flex::ItemOffset,
item_align : flex::ItemAlign,
stuff : AnyComponents,
}
impl ComponentTrait for Item {
fn new() -> Self {
Item::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.prepend_classes(
[
"pt-flex__item".to_owned(),
self.grow().to_string(),
self.shrink().to_string(),
self.size().to_string(),
self.offset().to_string(),
self.align().to_string(),
]
.join(" "),
);
self.inner_classes
.alter_value(ClassesOp::Prepend, "pt-flex__item-inner");
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let order = match self.weight() {
0 => None,
_ => Some(concat_string!("order: ", self.weight().to_string(), ";")),
};
PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] style=[order] {
div class=[self.inner_classes().get()] {
(self.components().render(cx))
}
}
})
}
}
impl Item {
// Item BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_inner_classes(&mut self, op: ClassesOp, classes: impl Into<String>) -> &mut Self {
self.inner_classes.alter_value(op, classes);
self
}
#[fn_with]
pub fn alter_grow(&mut self, grow: flex::ItemGrow) -> &mut Self {
self.item_grow = grow;
self
}
#[fn_with]
pub fn alter_shrink(&mut self, shrink: flex::ItemShrink) -> &mut Self {
self.item_shrink = shrink;
self
}
#[fn_with]
pub fn alter_size(&mut self, size: flex::ItemSize) -> &mut Self {
self.item_size = size;
self
}
#[fn_with]
pub fn alter_offset(&mut self, offset: flex::ItemOffset) -> &mut Self {
self.item_offset = offset;
self
}
#[fn_with]
pub fn alter_align(&mut self, align: flex::ItemAlign) -> &mut Self {
self.item_align = align;
self
}
#[rustfmt::skip]
pub fn add_component(mut self, component: impl ComponentTrait) -> Self {
self.stuff.alter_value(ArcAnyOp::Add(ArcAnyComponent::new(component)));
self
}
#[fn_with]
pub fn alter_components(&mut self, op: ArcAnyOp) -> &mut Self {
self.stuff.alter_value(op);
self
}
// Item GETTERS.
pub fn inner_classes(&self) -> &OptionClasses {
&self.inner_classes
}
pub fn grow(&self) -> &flex::ItemGrow {
&self.item_grow
}
pub fn shrink(&self) -> &flex::ItemShrink {
&self.item_shrink
}
pub fn size(&self) -> &flex::ItemSize {
&self.item_size
}
pub fn offset(&self) -> &flex::ItemOffset {
&self.item_offset
}
pub fn align(&self) -> &flex::ItemAlign {
&self.item_align
}
pub fn components(&self) -> &AnyComponents {
&self.stuff
}
}

View file

@ -0,0 +1,14 @@
mod form_main;
pub use form_main::{Form, FormMethod};
mod input;
pub use input::{Input, InputType};
mod hidden;
pub use hidden::Hidden;
mod date;
pub use date::Date;
mod action_button;
pub use action_button::{ActionButton, ActionButtonType};

View file

@ -0,0 +1,199 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(SmartDefault)]
pub enum ActionButtonType {
#[default]
Submit,
Reset,
}
#[rustfmt::skip]
impl ToString for ActionButtonType {
fn to_string(&self) -> String {
String::from(match self {
ActionButtonType::Submit => "submit",
ActionButtonType::Reset => "reset",
})
}
}
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct ActionButton {
weight : Weight,
renderable : Renderable,
classes : OptionClasses,
button_type: ActionButtonType,
style : ButtonStyle,
font_size : FontSize,
left_icon : OptionComponent<Icon>,
right_icon : OptionComponent<Icon>,
name : OptionString,
value : OptionTranslated,
autofocus : OptionString,
disabled : OptionString,
}
impl ComponentTrait for ActionButton {
fn new() -> Self {
ActionButton::submit()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.prepend_classes([self.style().to_string(), self.font_size().to_string()].join(" "));
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let id = self.name().get().map(|name| concat_string!("edit-", name));
PrepareMarkup::With(html! {
button
type=(self.button_type().to_string())
id=[id]
class=[self.classes().get()]
name=[self.name().get()]
value=[self.value().using(cx.langid())]
autofocus=[self.autofocus().get()]
disabled=[self.disabled().get()]
{
(self.left_icon().render(cx))
" " (self.value().escaped(cx.langid())) " "
(self.right_icon().render(cx))
}
})
}
}
impl ActionButton {
pub fn submit() -> Self {
ActionButton {
button_type: ActionButtonType::Submit,
style: ButtonStyle::Default,
value: OptionTranslated::new(L10n::l("button_submit")),
..Default::default()
}
}
pub fn reset() -> Self {
ActionButton {
button_type: ActionButtonType::Reset,
style: ButtonStyle::Info,
value: OptionTranslated::new(L10n::l("button_reset")),
..Default::default()
}
}
// Button BUILDER.
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_style(&mut self, style: ButtonStyle) -> &mut Self {
self.style = style;
self
}
#[fn_with]
pub fn alter_font_size(&mut self, font_size: FontSize) -> &mut Self {
self.font_size = font_size;
self
}
#[fn_with]
pub fn alter_left_icon(&mut self, icon: Option<Icon>) -> &mut Self {
self.left_icon.alter_value(icon);
self
}
#[fn_with]
pub fn alter_right_icon(&mut self, icon: Option<Icon>) -> &mut Self {
self.right_icon.alter_value(icon);
self
}
#[fn_with]
pub fn alter_name(&mut self, name: &str) -> &mut Self {
self.name.alter_value(name);
self
}
#[fn_with]
pub fn alter_value(&mut self, value: L10n) -> &mut Self {
self.value.alter_value(value);
self
}
#[fn_with]
pub fn alter_autofocus(&mut self, toggle: bool) -> &mut Self {
self.autofocus.alter_value(match toggle {
true => "autofocus",
false => "",
});
self
}
#[fn_with]
pub fn alter_disabled(&mut self, toggle: bool) -> &mut Self {
self.disabled.alter_value(match toggle {
true => "disabled",
false => "",
});
self
}
// Button GETTERS.
pub fn button_type(&self) -> &ActionButtonType {
&self.button_type
}
pub fn style(&self) -> &ButtonStyle {
&self.style
}
pub fn font_size(&self) -> &FontSize {
&self.font_size
}
pub fn left_icon(&self) -> &OptionComponent<Icon> {
&self.left_icon
}
pub fn right_icon(&self) -> &OptionComponent<Icon> {
&self.right_icon
}
pub fn name(&self) -> &OptionString {
&self.name
}
pub fn value(&self) -> &OptionTranslated {
&self.value
}
pub fn autofocus(&self) -> &OptionString {
&self.autofocus
}
pub fn disabled(&self) -> &OptionString {
&self.disabled
}
}

View file

@ -0,0 +1,200 @@
use crate::prelude::*;
use crate::BaseHandle;
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Date {
weight : Weight,
renderable : Renderable,
classes : OptionClasses,
name : OptionString,
value : OptionString,
label : OptionString,
placeholder : OptionString,
autofocus : OptionString,
autocomplete: OptionString,
disabled : OptionString,
readonly : OptionString,
required : OptionString,
help_text : OptionString,
}
impl ComponentTrait for Date {
fn new() -> Self {
Date::default().with_classes(ClassesOp::Add, "form-item form-type-date")
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
let id = self.name().get().map(|name| concat_string!("edit-", name));
PrepareMarkup::With(html! {
div class=[self.classes().get()] {
@if let Some(label) = self.label().get() {
label class="form-label" for=[&id] {
(label) " "
@if self.required().get().is_some() {
span
class="form-required"
title="Este campo es obligatorio." { "*" } " "
}
}
}
input
type="date"
id=[id]
class="form-control"
name=[self.name().get()]
value=[self.value().get()]
placeholder=[self.placeholder().get()]
autofocus=[self.autofocus().get()]
autocomplete=[self.autocomplete().get()]
readonly=[self.readonly().get()]
required=[self.required().get()]
disabled=[self.disabled().get()] {}
@if let Some(help_text) = self.help_text().get() {
div class="form-text" { (help_text) }
}
}
})
}
}
impl Date {
// Date BUILDER.
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_name(&mut self, name: &str) -> &mut Self {
self.name.alter_value(name);
self
}
#[fn_with]
pub fn alter_value(&mut self, value: &str) -> &mut Self {
self.value.alter_value(value);
self
}
#[fn_with]
pub fn alter_label(&mut self, label: &str) -> &mut Self {
self.label.alter_value(label);
self
}
#[fn_with]
pub fn alter_placeholder(&mut self, placeholder: &str) -> &mut Self {
self.placeholder.alter_value(placeholder);
self
}
#[fn_with]
pub fn alter_autofocus(&mut self, toggle: bool) -> &mut Self {
self.autofocus.alter_value(match toggle {
true => "autofocus",
false => "",
});
self
}
#[fn_with]
pub fn alter_autocomplete(&mut self, toggle: bool) -> &mut Self {
self.autocomplete.alter_value(match toggle {
true => "",
false => "off",
});
self
}
#[fn_with]
pub fn alter_disabled(&mut self, toggle: bool) -> &mut Self {
self.disabled.alter_value(match toggle {
true => "disabled",
false => "",
});
self
}
#[fn_with]
pub fn alter_readonly(&mut self, toggle: bool) -> &mut Self {
self.readonly.alter_value(match toggle {
true => "readonly",
false => "",
});
self
}
#[fn_with]
pub fn alter_required(&mut self, toggle: bool) -> &mut Self {
self.required.alter_value(match toggle {
true => "required",
false => "",
});
self
}
#[fn_with]
pub fn alter_help_text(&mut self, help_text: &str) -> &mut Self {
self.help_text.alter_value(help_text);
self
}
// Date GETTERS.
pub fn name(&self) -> &OptionString {
&self.name
}
pub fn value(&self) -> &OptionString {
&self.value
}
pub fn label(&self) -> &OptionString {
&self.label
}
pub fn placeholder(&self) -> &OptionString {
&self.placeholder
}
pub fn autofocus(&self) -> &OptionString {
&self.autofocus
}
pub fn autocomplete(&self) -> &OptionString {
&self.autocomplete
}
pub fn disabled(&self) -> &OptionString {
&self.disabled
}
pub fn readonly(&self) -> &OptionString {
&self.readonly
}
pub fn required(&self) -> &OptionString {
&self.required
}
pub fn help_text(&self) -> &OptionString {
&self.help_text
}
}

View file

@ -0,0 +1,130 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(SmartDefault)]
pub enum FormMethod {
#[default]
Post,
Get,
}
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Form {
id : OptionId,
weight : Weight,
renderable: Renderable,
classes : OptionClasses,
action : OptionString,
charset : OptionString,
method : FormMethod,
stuff : AnyComponents,
}
impl ComponentTrait for Form {
fn new() -> Self {
Form::default()
.with_classes(ClassesOp::Add, "form")
.with_charset("UTF-8")
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let method = match self.method() {
FormMethod::Post => Some("post".to_owned()),
FormMethod::Get => None,
};
PrepareMarkup::With(html! {
form
id=[self.id()]
class=[self.classes().get()]
action=[self.action().get()]
method=[method]
accept-charset=[self.charset().get()]
{
div { (self.elements().render(cx)) }
}
})
}
}
impl Form {
// Form BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_action(&mut self, action: &str) -> &mut Self {
self.action.alter_value(action);
self
}
#[fn_with]
pub fn alter_charset(&mut self, charset: &str) -> &mut Self {
self.charset.alter_value(charset);
self
}
#[fn_with]
pub fn alter_method(&mut self, method: FormMethod) -> &mut Self {
self.method = method;
self
}
#[rustfmt::skip]
pub fn with_element(mut self, element: impl ComponentTrait) -> Self {
self.stuff.alter_value(ArcAnyOp::Add(ArcAnyComponent::new(element)));
self
}
#[fn_with]
pub fn alter_elements(&mut self, op: ArcAnyOp) -> &mut Self {
self.stuff.alter_value(op);
self
}
// Form GETTERS.
pub fn action(&self) -> &OptionString {
&self.action
}
pub fn charset(&self) -> &OptionString {
&self.charset
}
pub fn method(&self) -> &FormMethod {
&self.method
}
pub fn elements(&self) -> &AnyComponents {
&self.stuff
}
}

View file

@ -0,0 +1,63 @@
use crate::prelude::*;
use crate::BaseHandle;
#[rustfmt::skip]
#[derive(BaseHandle, SmartDefault)]
pub struct Hidden {
weight: Weight,
name : OptionName,
value : OptionString,
}
impl ComponentTrait for Hidden {
fn new() -> Self {
Hidden::default()
}
fn weight(&self) -> Weight {
self.weight
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
let id = self.name().get().map(|name| concat_string!("value-", name));
PrepareMarkup::With(html! {
input type="hidden" id=[id] name=[self.name().get()] value=[self.value().get()] {}
})
}
}
impl Hidden {
pub fn set(name: &str, value: &str) -> Self {
Hidden::default().with_name(name).with_value(value)
}
// Hidden BUILDER.
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_name(&mut self, name: &str) -> &mut Self {
self.name.alter_value(name);
self
}
#[fn_with]
pub fn alter_value(&mut self, value: &str) -> &mut Self {
self.value.alter_value(value);
self
}
// Hidden GETTERS.
pub fn name(&self) -> &OptionName {
&self.name
}
pub fn value(&self) -> &OptionString {
&self.value
}
}

View file

@ -0,0 +1,317 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(SmartDefault)]
pub enum InputType {
#[default]
Textfield,
Password,
Search,
Email,
Telephone,
Url,
}
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Input {
weight : Weight,
renderable : Renderable,
classes : OptionClasses,
input_type : InputType,
name : OptionName,
value : OptionString,
label : OptionTranslated,
size : Option<u16>,
minlength : Option<u16>,
maxlength : Option<u16>,
placeholder : OptionString,
autofocus : OptionString,
autocomplete: OptionString,
disabled : OptionString,
readonly : OptionString,
required : OptionString,
help_text : OptionTranslated,
}
impl ComponentTrait for Input {
fn new() -> Self {
Input::default()
.with_classes(ClassesOp::Add, "form-item form-type-textfield")
.with_size(Some(60))
.with_maxlength(Some(128))
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
#[rustfmt::skip]
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let type_input = match self.input_type() {
InputType::Textfield => "text",
InputType::Password => "password",
InputType::Search => "search",
InputType::Email => "email",
InputType::Telephone => "tel",
InputType::Url => "url",
};
let id = self.name().get().map(|name| concat_string!("edit-", name));
PrepareMarkup::With(html! {
div class=[self.classes().get()] {
@if let Some(label) = self.label().using(cx.langid()) {
label class="form-label" for=[&id] {
(label) " "
@if self.required().get().is_some() {
span
class="form-required"
title="Este campo es obligatorio." { "*" } " "
}
}
}
input
type=(type_input)
id=[id]
class="form-control"
name=[self.name().get()]
value=[self.value().get()]
size=[self.size()]
minlength=[self.minlength()]
maxlength=[self.maxlength()]
placeholder=[self.placeholder().get()]
autofocus=[self.autofocus().get()]
autocomplete=[self.autocomplete().get()]
readonly=[self.readonly().get()]
required=[self.required().get()]
disabled=[self.disabled().get()] {}
@if let Some(description) = self.help_text().using(cx.langid()) {
div class="form-text" { (description) }
}
}
})
}
}
impl Input {
pub fn textfield() -> Self {
Input::default()
}
pub fn password() -> Self {
let mut input = Input::default().with_classes(
ClassesOp::Replace("form-type-textfield".to_owned()),
"form-type-password",
);
input.input_type = InputType::Password;
input
}
pub fn search() -> Self {
let mut input = Input::default().with_classes(
ClassesOp::Replace("form-type-textfield".to_owned()),
"form-type-search",
);
input.input_type = InputType::Search;
input
}
pub fn email() -> Self {
let mut input = Input::default().with_classes(
ClassesOp::Replace("form-type-textfield".to_owned()),
"form-type-email",
);
input.input_type = InputType::Email;
input
}
pub fn telephone() -> Self {
let mut input = Input::default().with_classes(
ClassesOp::Replace("form-type-textfield".to_owned()),
"form-type-telephone",
);
input.input_type = InputType::Telephone;
input
}
pub fn url() -> Self {
let mut input = Input::default().with_classes(
ClassesOp::Replace("form-type-textfield".to_owned()),
"form-type-url",
);
input.input_type = InputType::Url;
input
}
// Input BUILDER.
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_name(&mut self, name: &str) -> &mut Self {
if let Some(previous) = self.name.get() {
self.remove_classes(concat_string!("form-item-", previous));
}
self.add_classes(concat_string!("form-item-", name));
self.name.alter_value(name);
self
}
#[fn_with]
pub fn alter_value(&mut self, value: &str) -> &mut Self {
self.value.alter_value(value);
self
}
#[fn_with]
pub fn alter_label(&mut self, label: L10n) -> &mut Self {
self.label.alter_value(label);
self
}
#[fn_with]
pub fn alter_size(&mut self, size: Option<u16>) -> &mut Self {
self.size = size;
self
}
#[fn_with]
pub fn alter_minlength(&mut self, minlength: Option<u16>) -> &mut Self {
self.minlength = minlength;
self
}
#[fn_with]
pub fn alter_maxlength(&mut self, maxlength: Option<u16>) -> &mut Self {
self.maxlength = maxlength;
self
}
#[fn_with]
pub fn alter_placeholder(&mut self, placeholder: &str) -> &mut Self {
self.placeholder.alter_value(placeholder);
self
}
#[fn_with]
pub fn alter_autofocus(&mut self, toggle: bool) -> &mut Self {
self.autofocus.alter_value(match toggle {
true => "autofocus",
false => "",
});
self
}
#[fn_with]
pub fn alter_autocomplete(&mut self, toggle: bool) -> &mut Self {
self.autocomplete.alter_value(match toggle {
true => "",
false => "off",
});
self
}
#[fn_with]
pub fn alter_disabled(&mut self, toggle: bool) -> &mut Self {
self.disabled.alter_value(match toggle {
true => "disabled",
false => "",
});
self
}
#[fn_with]
pub fn alter_readonly(&mut self, toggle: bool) -> &mut Self {
self.readonly.alter_value(match toggle {
true => "readonly",
false => "",
});
self
}
#[fn_with]
pub fn alter_required(&mut self, toggle: bool) -> &mut Self {
self.required.alter_value(match toggle {
true => "required",
false => "",
});
self
}
#[fn_with]
pub fn alter_help_text(&mut self, help_text: L10n) -> &mut Self {
self.help_text.alter_value(help_text);
self
}
// Input GETTERS.
pub fn input_type(&self) -> &InputType {
&self.input_type
}
pub fn name(&self) -> &OptionName {
&self.name
}
pub fn value(&self) -> &OptionString {
&self.value
}
pub fn label(&self) -> &OptionTranslated {
&self.label
}
pub fn size(&self) -> Option<u16> {
self.size
}
pub fn minlength(&self) -> Option<u16> {
self.minlength
}
pub fn maxlength(&self) -> Option<u16> {
self.maxlength
}
pub fn placeholder(&self) -> &OptionString {
&self.placeholder
}
pub fn autofocus(&self) -> &OptionString {
&self.autofocus
}
pub fn autocomplete(&self) -> &OptionString {
&self.autocomplete
}
pub fn disabled(&self) -> &OptionString {
&self.disabled
}
pub fn readonly(&self) -> &OptionString {
&self.readonly
}
pub fn required(&self) -> &OptionString {
&self.required
}
pub fn help_text(&self) -> &OptionTranslated {
&self.help_text
}
}

View file

@ -0,0 +1,178 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(SmartDefault)]
pub enum HeadingType {
#[default]
H1,
H2,
H3,
H4,
H5,
H6,
}
#[derive(SmartDefault)]
pub enum HeadingSize {
ExtraLarge,
XxLarge,
XLarge,
Large,
Medium,
#[default]
Normal,
Subtitle,
}
#[rustfmt::skip]
impl ToString for HeadingSize {
fn to_string(&self) -> String {
String::from(match self {
HeadingSize::ExtraLarge => "pt-heading__title-x3l",
HeadingSize::XxLarge => "pt-heading__title-x2l",
HeadingSize::XLarge => "pt-heading__title-xl",
HeadingSize::Large => "pt-heading__title-l",
HeadingSize::Medium => "pt-heading__title-m",
HeadingSize::Normal => "",
HeadingSize::Subtitle => "pt-heading__subtitle",
})
}
}
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Heading {
id : OptionId,
weight : Weight,
renderable : Renderable,
classes : OptionClasses,
heading_type: HeadingType,
size : HeadingSize,
text : OptionTranslated,
}
impl ComponentTrait for Heading {
fn new() -> Self {
Heading::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.add_classes(self.size().to_string());
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let id = self.id();
let classes = self.classes().get();
let text = self.text().escaped(cx.langid());
PrepareMarkup::With(html! { @match &self.heading_type() {
HeadingType::H1 => h1 id=[id] class=[classes] { (text) },
HeadingType::H2 => h2 id=[id] class=[classes] { (text) },
HeadingType::H3 => h3 id=[id] class=[classes] { (text) },
HeadingType::H4 => h4 id=[id] class=[classes] { (text) },
HeadingType::H5 => h5 id=[id] class=[classes] { (text) },
HeadingType::H6 => h6 id=[id] class=[classes] { (text) },
}})
}
}
impl Heading {
pub fn h1(text: L10n) -> Self {
Heading::default()
.with_heading_type(HeadingType::H1)
.with_text(text)
}
pub fn h2(text: L10n) -> Self {
Heading::default()
.with_heading_type(HeadingType::H2)
.with_text(text)
}
pub fn h3(text: L10n) -> Self {
Heading::default()
.with_heading_type(HeadingType::H3)
.with_text(text)
}
pub fn h4(text: L10n) -> Self {
Heading::default()
.with_heading_type(HeadingType::H4)
.with_text(text)
}
pub fn h5(text: L10n) -> Self {
Heading::default()
.with_heading_type(HeadingType::H5)
.with_text(text)
}
pub fn h6(text: L10n) -> Self {
Heading::default()
.with_heading_type(HeadingType::H6)
.with_text(text)
}
// Heading BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_heading_type(&mut self, heading_type: HeadingType) -> &mut Self {
self.heading_type = heading_type;
self
}
#[fn_with]
pub fn alter_size(&mut self, size: HeadingSize) -> &mut Self {
self.size = size;
self
}
#[fn_with]
pub fn alter_text(&mut self, text: L10n) -> &mut Self {
self.text.alter_value(text);
self
}
// Paragraph GETTERS.
pub fn heading_type(&self) -> &HeadingType {
&self.heading_type
}
pub fn size(&self) -> &HeadingSize {
&self.size
}
pub fn text(&self) -> &OptionTranslated {
&self.text
}
}

View file

@ -0,0 +1,35 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(BaseHandle, SmartDefault)]
pub struct Html(Markup);
impl ComponentTrait for Html {
fn new() -> Self {
Html::default()
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! { (self.html()) })
}
}
impl Html {
pub fn with(html: Markup) -> Self {
Html(html)
}
// Html BUILDER.
#[fn_with]
pub fn alter_html(&mut self, html: Markup) -> &mut Self {
self.0 = html;
self
}
// Html GETTERS.
pub fn html(&self) -> &Markup {
&self.0
}
}

View file

@ -0,0 +1,85 @@
use crate::prelude::*;
use crate::BaseHandle;
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Icon {
weight : Weight,
renderable: Renderable,
classes : OptionClasses,
icon_name : OptionString,
font_size : FontSize,
}
impl ComponentTrait for Icon {
fn new() -> Self {
Icon::default()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
#[rustfmt::skip]
fn setup_before_prepare(&mut self, cx: &mut Context) {
if let Some(icon_name) = self.icon_name().get() {
self.prepend_classes(
concat_string!("bi-", icon_name, " ", self.font_size().to_string()),
);
cx.set_param::<bool>(PARAM_BASE_INCLUDE_ICONS, true);
}
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
match self.icon_name().get() {
None => PrepareMarkup::None,
_ => PrepareMarkup::With(html! { i class=[self.classes().get()] {} }),
}
}
}
impl Icon {
pub fn with(icon_name: impl Into<String>) -> Self {
Icon::default().with_icon_name(icon_name)
}
// Icon BUILDER.
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_icon_name(&mut self, name: impl Into<String>) -> &mut Self {
self.icon_name.alter_value(name);
self
}
#[fn_with]
pub fn alter_font_size(&mut self, font_size: FontSize) -> &mut Self {
self.font_size = font_size;
self
}
// Icon GETTERS.
pub fn icon_name(&self) -> &OptionString {
&self.icon_name
}
pub fn font_size(&self) -> &FontSize {
&self.font_size
}
}

125
src/base/component/image.rs Normal file
View file

@ -0,0 +1,125 @@
use crate::prelude::*;
use crate::BaseHandle;
const IMG_FLUID: &str = "pt-img__fluid";
const IMG_FIXED: &str = "pt-img__fixed";
#[derive(SmartDefault)]
pub enum ImageSize {
#[default]
Auto,
Size(u16, u16),
Width(u16),
Height(u16),
Both(u16),
}
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Image {
id : OptionId,
weight : Weight,
renderable: Renderable,
classes : OptionClasses,
source : OptionString,
size : ImageSize,
}
impl ComponentTrait for Image {
fn new() -> Self {
Image::default().with_classes(ClassesOp::Add, IMG_FLUID)
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
let (width, height) = match self.size() {
ImageSize::Auto => (None, None),
ImageSize::Size(width, height) => (Some(width), Some(height)),
ImageSize::Width(width) => (Some(width), None),
ImageSize::Height(height) => (None, Some(height)),
ImageSize::Both(value) => (Some(value), Some(value)),
};
PrepareMarkup::With(html! {
img
src=[self.source().get()]
id=[self.id()]
class=[self.classes().get()]
width=[width]
height=[height] {}
})
}
}
impl Image {
pub fn with(source: &str) -> Self {
Image::default()
.with_source(source)
.with_classes(ClassesOp::Add, IMG_FLUID)
}
pub fn fixed(source: &str) -> Self {
Image::default()
.with_source(source)
.with_classes(ClassesOp::Add, IMG_FIXED)
}
pub fn pagetop() -> Self {
Image::default()
.with_source("/base/pagetop-logo.svg")
.with_classes(ClassesOp::Add, IMG_FIXED)
.with_size(ImageSize::Size(64, 64))
}
// Image BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_source(&mut self, source: &str) -> &mut Self {
self.source.alter_value(source);
self
}
#[fn_with]
pub fn alter_size(&mut self, size: ImageSize) -> &mut Self {
self.size = size;
self
}
// Image GETTERS.
pub fn source(&self) -> &OptionString {
&self.source
}
pub fn size(&self) -> &ImageSize {
&self.size
}
}

View file

@ -0,0 +1,17 @@
mod menu_main;
pub use menu_main::Menu;
mod item;
pub use item::{Item, ItemType};
mod submenu;
pub use submenu::Submenu;
mod megamenu;
pub use megamenu::Megamenu;
mod group;
pub use group::Group;
mod element;
pub use element::{Element, ElementType};

View file

@ -0,0 +1,87 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::Submenu;
type Content = ArcTypedComponent<Html>;
type SubmenuItems = ArcTypedComponent<Submenu>;
#[derive(SmartDefault)]
pub enum ElementType {
#[default]
Void,
Html(Content),
Submenu(SubmenuItems),
}
// Element.
#[rustfmt::skip]
#[derive(BaseHandle, SmartDefault)]
pub struct Element {
weight : Weight,
renderable : Renderable,
element_type: ElementType,
}
impl ComponentTrait for Element {
fn new() -> Self {
Element::default()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.element_type() {
ElementType::Void => PrepareMarkup::None,
ElementType::Html(content) => PrepareMarkup::With(html! {
(content.render(cx))
}),
ElementType::Submenu(submenu) => PrepareMarkup::With(html! {
(submenu.render(cx))
}),
}
}
}
impl Element {
pub fn html(content: Html) -> Self {
Element {
element_type: ElementType::Html(Content::new(content)),
..Default::default()
}
}
pub fn submenu(submenu: Submenu) -> Self {
Element {
element_type: ElementType::Submenu(SubmenuItems::new(submenu)),
..Default::default()
}
}
// Element BUILDER.
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
// Element GETTERS.
pub fn element_type(&self) -> &ElementType {
&self.element_type
}
}

View file

@ -0,0 +1,79 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::Element;
#[rustfmt::skip]
#[derive(BaseHandle, SmartDefault)]
pub struct Group {
id : OptionId,
weight : Weight,
renderable: Renderable,
elements : TypedComponents<Element>,
}
impl ComponentTrait for Group {
fn new() -> Self {
Group::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div id=[self.id()] class="menu-group" {
(self.elements().render(cx))
}
})
}
}
impl Group {
// Group BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[rustfmt::skip]
pub fn add_element(mut self, element: Element) -> Self {
self.elements.alter_value(ArcTypedOp::Add(ArcTypedComponent::new(element)));
self
}
#[fn_with]
pub fn alter_elements(&mut self, op: ArcTypedOp<Element>) -> &mut Self {
self.elements.alter_value(op);
self
}
// Group GETTERS.
pub fn elements(&self) -> &TypedComponents<Element> {
&self.elements
}
}

View file

@ -0,0 +1,207 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::{Megamenu, Submenu};
type Label = L10n;
type Content = ArcTypedComponent<Html>;
type SubmenuItems = ArcTypedComponent<Submenu>;
type MegamenuGroups = ArcTypedComponent<Megamenu>;
#[derive(SmartDefault)]
pub enum ItemType {
#[default]
Void,
Label(Label),
Link(Label, FnContextualPath),
LinkBlank(Label, FnContextualPath),
Html(Content),
Submenu(Label, SubmenuItems),
Megamenu(Label, MegamenuGroups),
}
// Item.
#[rustfmt::skip]
#[derive(BaseHandle, SmartDefault)]
pub struct Item {
weight : Weight,
renderable : Renderable,
item_type : ItemType,
description: OptionTranslated,
left_icon : OptionComponent<Icon>,
right_icon : OptionComponent<Icon>,
}
impl ComponentTrait for Item {
fn new() -> Self {
Item::default()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let description = self.description.using(cx.langid());
let left_icon = self.left_icon().render(cx);
let right_icon = self.right_icon().render(cx);
match self.item_type() {
ItemType::Void => PrepareMarkup::None,
ItemType::Label(label) => PrepareMarkup::With(html! {
li class="pt-menu__label" {
span title=[description] {
(left_icon)
(label.escaped(cx.langid()))
(right_icon)
}
}
}),
ItemType::Link(label, path) => PrepareMarkup::With(html! {
li class="pt-menu__link" {
a href=(path(cx)) title=[description] {
(left_icon)
(label.escaped(cx.langid()))
(right_icon)
}
}
}),
ItemType::LinkBlank(label, path) => PrepareMarkup::With(html! {
li class="pt-menu__link" {
a href=(path(cx)) title=[description] target="_blank" {
(left_icon)
(label.escaped(cx.langid()))
(right_icon)
}
}
}),
ItemType::Html(content) => PrepareMarkup::With(html! {
li class="pt-menu__html" {
(content.render(cx))
}
}),
ItemType::Submenu(label, submenu) => PrepareMarkup::With(html! {
li class="pt-menu__children" {
a href="#" title=[description] {
(left_icon)
(label.escaped(cx.langid())) i class="pt-menu__icon bi-chevron-down" {}
}
div class="pt-menu__subs" {
(submenu.render(cx))
}
}
}),
ItemType::Megamenu(label, megamenu) => PrepareMarkup::With(html! {
li class="pt-menu__children" {
a href="#" title=[description] {
(left_icon)
(label.escaped(cx.langid())) i class="pt-menu__icon bi-chevron-down" {}
}
div class="pt-menu__subs pt-menu__mega" {
(megamenu.render(cx))
}
}
}),
}
}
}
impl Item {
pub fn label(label: L10n) -> Self {
Item {
item_type: ItemType::Label(label),
..Default::default()
}
}
pub fn link(label: L10n, path: FnContextualPath) -> Self {
Item {
item_type: ItemType::Link(label, path),
..Default::default()
}
}
pub fn link_blank(label: L10n, path: FnContextualPath) -> Self {
Item {
item_type: ItemType::LinkBlank(label, path),
..Default::default()
}
}
pub fn html(content: Html) -> Self {
Item {
item_type: ItemType::Html(Content::new(content)),
..Default::default()
}
}
pub fn submenu(label: L10n, submenu: Submenu) -> Self {
Item {
item_type: ItemType::Submenu(label, SubmenuItems::new(submenu)),
..Default::default()
}
}
pub fn megamenu(label: L10n, megamenu: Megamenu) -> Self {
Item {
item_type: ItemType::Megamenu(label, MegamenuGroups::new(megamenu)),
..Default::default()
}
}
// Item BUILDER.
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_description(&mut self, text: L10n) -> &mut Self {
self.description.alter_value(text);
self
}
#[fn_with]
pub fn alter_left_icon(&mut self, icon: Option<Icon>) -> &mut Self {
self.left_icon.alter_value(icon);
self
}
#[fn_with]
pub fn alter_right_icon(&mut self, icon: Option<Icon>) -> &mut Self {
self.right_icon.alter_value(icon);
self
}
// Item GETTERS.
pub fn item_type(&self) -> &ItemType {
&self.item_type
}
pub fn description(&self) -> &OptionTranslated {
&self.description
}
pub fn left_icon(&self) -> &OptionComponent<Icon> {
&self.left_icon
}
pub fn right_icon(&self) -> &OptionComponent<Icon> {
&self.right_icon
}
}

View file

@ -0,0 +1,79 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::Group;
#[rustfmt::skip]
#[derive(BaseHandle, SmartDefault)]
pub struct Megamenu {
id : OptionId,
weight : Weight,
renderable: Renderable,
groups : TypedComponents<Group>,
}
impl ComponentTrait for Megamenu {
fn new() -> Self {
Megamenu::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div id=[self.id()] class="pt-menu__groups" {
(self.groups().render(cx))
}
})
}
}
impl Megamenu {
// Megamenu BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[rustfmt::skip]
pub fn add_group(mut self, group: Group) -> Self {
self.groups.alter_value(ArcTypedOp::Add(ArcTypedComponent::new(group)));
self
}
#[fn_with]
pub fn alter_groups(&mut self, op: ArcTypedOp<Group>) -> &mut Self {
self.groups.alter_value(op);
self
}
// Megamenu GETTERS.
pub fn groups(&self) -> &TypedComponents<Group> {
&self.groups
}
}

View file

@ -0,0 +1,107 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::Item;
#[rustfmt::skip]
#[derive(BaseHandle, SmartDefault)]
pub struct Menu {
id : OptionId,
weight : Weight,
renderable: Renderable,
items : TypedComponents<Item>,
}
impl ComponentTrait for Menu {
fn new() -> Self {
Menu::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
cx.set_param::<bool>(PARAM_BASE_INCLUDE_MENU_ASSETS, true);
cx.set_param::<bool>(PARAM_BASE_INCLUDE_ICONS, true);
PrepareMarkup::With(html! {
div id=[self.id()] class="pt-menu__container" {
div class="pt-menu__wrapper" {
div class="pt-menu__main" {
div class="pt-menu__overlay" {}
nav class="pt-menu__nav" {
div class="pt-menu__header" {
button type="button" class="pt-menu__arrow" {
i class="bi-chevron-left" {}
}
div class="pt-menu__title" {}
button type="button" class="pt-menu__close" {
i class="bi-x" {}
}
}
ul class="pt-menu__section" {
(self.items().render(cx))
}
}
}
button
type="button"
class="pt-menu__trigger"
title=[L10n::l("menu_toggle").using(cx.langid())]
{
span {} span {} span {}
}
}
}
})
}
}
impl Menu {
// Menu BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[rustfmt::skip]
pub fn add_item(mut self, item: Item) -> Self {
self.items.alter_value(ArcTypedOp::Add(ArcTypedComponent::new(item)));
self
}
#[fn_with]
pub fn alter_items(&mut self, op: ArcTypedOp<Item>) -> &mut Self {
self.items.alter_value(op);
self
}
// Menu GETTERS.
pub fn items(&self) -> &TypedComponents<Item> {
&self.items
}
}

View file

@ -0,0 +1,95 @@
use crate::prelude::*;
use crate::BaseHandle;
use super::Item;
#[rustfmt::skip]
#[derive(BaseHandle, SmartDefault)]
pub struct Submenu {
id : OptionId,
weight : Weight,
renderable: Renderable,
title : OptionTranslated,
items : TypedComponents<Item>,
}
impl ComponentTrait for Submenu {
fn new() -> Self {
Submenu::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
div id=[self.id()] class="pt-menu__items" {
@if let Some(title) = self.title().using(cx.langid()) {
h4 class="pt-menu__title" { (title) }
}
ul {
(self.items().render(cx))
}
}
})
}
}
impl Submenu {
// Submenu BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_title(&mut self, title: L10n) -> &mut Self {
self.title.alter_value(title);
self
}
#[rustfmt::skip]
pub fn add_item(mut self, item: Item) -> Self {
self.items.alter_value(ArcTypedOp::Add(ArcTypedComponent::new(item)));
self
}
#[fn_with]
pub fn alter_items(&mut self, op: ArcTypedOp<Item>) -> &mut Self {
self.items.alter_value(op);
self
}
// Submenu GETTERS.
pub fn title(&self) -> &OptionTranslated {
&self.title
}
pub fn items(&self) -> &TypedComponents<Item> {
&self.items
}
}

View file

@ -0,0 +1,110 @@
use crate::prelude::*;
use crate::BaseHandle;
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Paragraph {
id : OptionId,
weight : Weight,
renderable: Renderable,
classes : OptionClasses,
font_size : FontSize,
stuff : AnyComponents,
}
impl ComponentTrait for Paragraph {
fn new() -> Self {
Paragraph::default()
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn setup_before_prepare(&mut self, _cx: &mut Context) {
self.prepend_classes(self.font_size().to_string());
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(html! {
p
id=[self.id()]
class=[self.classes().get()]
{
(self.components().render(cx))
}
})
}
}
impl Paragraph {
pub fn with(component: impl ComponentTrait) -> Self {
Paragraph::default().add_component(component)
}
pub fn translated(l10n: L10n) -> Self {
Paragraph::default().add_translated(l10n)
}
// Paragraph BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_font_size(&mut self, font_size: FontSize) -> &mut Self {
self.font_size = font_size;
self
}
#[rustfmt::skip]
pub fn add_component(mut self, component: impl ComponentTrait) -> Self {
self.stuff.alter_value(ArcAnyOp::Add(ArcAnyComponent::new(component)));
self
}
#[rustfmt::skip]
pub fn add_translated(mut self, l10n: L10n) -> Self {
self.stuff.alter_value(ArcAnyOp::Add(ArcAnyComponent::new(Translate::with(l10n))));
self
}
#[fn_with]
pub fn alter_components(&mut self, op: ArcAnyOp) -> &mut Self {
self.stuff.alter_value(op);
self
}
// Paragraph GETTERS.
pub fn font_size(&self) -> &FontSize {
&self.font_size
}
pub fn components(&self) -> &AnyComponents {
&self.stuff
}
}

View file

@ -0,0 +1,133 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(Default, Eq, PartialEq)]
pub enum PoweredByLogo {
#[default]
None,
Color,
LineDark,
LineLight,
LineRGB(u8, u8, u8),
}
#[rustfmt::skip]
#[derive(BaseHandle, SmartDefault)]
pub struct PoweredBy {
weight : Weight,
renderable: Renderable,
copyright : Option<String>,
logo : PoweredByLogo,
}
impl ComponentTrait for PoweredBy {
fn new() -> Self {
let year = Utc::now().format("%Y").to_string();
let c = concat_string!(year, " © ", config::SETTINGS.app.name);
PoweredBy {
copyright: Some(c),
..Default::default()
}
}
fn id(&self) -> Option<String> {
Some("pt-poweredby".to_owned())
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let poweredby_pagetop = L10n::l("poweredby_pagetop")
.with_arg(
"pagetop_link",
"<a href=\"https://crates.io/crates/pagetop\">PageTop</a>",
)
.escaped(cx.langid());
let pagetop_logo = match self.logo() {
PoweredByLogo::None => html! {},
PoweredByLogo::Color => self.logo_color(cx),
PoweredByLogo::LineDark => self.logo_line(10, 11, 9, cx),
PoweredByLogo::LineLight => self.logo_line(255, 255, 255, cx),
PoweredByLogo::LineRGB(r, g, b) => self.logo_line(*r, *g, *b, cx),
};
PrepareMarkup::With(html! {
div id=[self.id()] {
@if let Some(c) = self.copyright() {
span class="pt-poweredby__copyright" { (c) "." } " "
}
span class="pt-poweredby__pagetop" { (poweredby_pagetop) " " (pagetop_logo) }
}
})
}
}
impl PoweredBy {
// PoweredBy BUILDER.
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_copyright(&mut self, copyright: Option<impl Into<String>>) -> &mut Self {
self.copyright = copyright.map(|c| c.into());
self
}
#[fn_with]
pub fn alter_logo(&mut self, logo: PoweredByLogo) -> &mut Self {
self.logo = logo;
self
}
// PoweredBy GETTERS.
pub fn copyright(&self) -> &Option<String> {
&self.copyright
}
pub fn logo(&self) -> &PoweredByLogo {
&self.logo
}
// PoweredBy PRIVATE.
fn logo_color(&self, cx: &mut Context) -> Markup {
let logo_txt = &L10n::l("pagetop_logo").using(cx.langid());
html! {
span class="pt-poweredby__logo" aria-label=[logo_txt] {
img src="/base/pagetop-logo.svg" alt=[logo_txt] {}
}
}
}
fn logo_line(&self, r: u8, g: u8, b: u8, cx: &mut Context) -> Markup {
let logo_txt = L10n::l("pagetop_logo").using(cx.langid());
let logo_rgb = format!("rgb({},{},{})", r, g, b);
html! {
span class="pt-poweredby__logo" aria-label=[logo_txt] {
svg viewBox="0 0 1614 1614" xmlns="http://www.w3.org/2000/svg" role="img" {
path fill=(logo_rgb) d="M 1573,357 L 1415,357 C 1400,357 1388,369 1388,383 L 1388,410 1335,410 1335,357 C 1335,167 1181,13 992,13 L 621,13 C 432,13 278,167 278,357 L 278,410 225,410 225,383 C 225,369 213,357 198,357 L 40,357 C 25,357 13,369 13,383 L 13,648 C 13,662 25,674 40,674 L 198,674 C 213,674 225,662 225,648 L 225,621 278,621 278,1256 C 278,1446 432,1600 621,1600 L 992,1600 C 1181,1600 1335,1446 1335,1256 L 1335,621 1388,621 1388,648 C 1388,662 1400,674 1415,674 L 1573,674 C 1588,674 1600,662 1600,648 L 1600,383 C 1600,369 1588,357 1573,357 L 1573,357 1573,357 Z M 66,410 L 172,410 172,621 66,621 66,410 66,410 Z M 1282,357 L 1282,488 C 1247,485 1213,477 1181,464 L 1196,437 C 1203,425 1199,409 1186,401 1174,394 1158,398 1150,411 L 1133,440 C 1105,423 1079,401 1056,376 L 1075,361 C 1087,352 1089,335 1079,324 1070,313 1054,311 1042,320 L 1023,335 C 1000,301 981,263 967,221 L 1011,196 C 1023,189 1028,172 1021,160 1013,147 997,143 984,150 L 953,168 C 945,136 941,102 940,66 L 992,66 C 1152,66 1282,197 1282,357 L 1282,357 1282,357 Z M 621,66 L 674,66 674,225 648,225 C 633,225 621,237 621,251 621,266 633,278 648,278 L 674,278 674,357 648,357 C 633,357 621,369 621,383 621,398 633,410 648,410 L 674,410 674,489 648,489 C 633,489 621,501 621,516 621,530 633,542 648,542 L 664,542 C 651,582 626,623 600,662 583,653 563,648 542,648 469,648 410,707 410,780 410,787 411,794 412,801 388,805 361,806 331,806 L 331,357 C 331,197 461,66 621,66 L 621,66 621,66 Z M 621,780 C 621,824 586,859 542,859 498,859 463,824 463,780 463,736 498,701 542,701 586,701 621,736 621,780 L 621,780 621,780 Z M 225,463 L 278,463 278,569 225,569 225,463 225,463 Z M 992,1547 L 621,1547 C 461,1547 331,1416 331,1256 L 331,859 C 367,859 400,858 431,851 454,888 495,912 542,912 615,912 674,853 674,780 674,747 662,718 642,695 675,645 706,594 720,542 L 780,542 C 795,542 807,530 807,516 807,501 795,489 780,489 L 727,489 727,410 780,410 C 795,410 807,398 807,383 807,369 795,357 780,357 L 727,357 727,278 780,278 C 795,278 807,266 807,251 807,237 795,225 780,225 L 727,225 727,66 887,66 C 889,111 895,155 905,196 L 869,217 C 856,224 852,240 859,253 864,261 873,266 882,266 887,266 891,265 895,263 L 921,248 C 937,291 958,331 983,367 L 938,403 C 926,412 925,429 934,440 939,447 947,450 954,450 960,450 966,448 971,444 L 1016,408 C 1043,438 1074,465 1108,485 L 1084,527 C 1076,539 1081,555 1093,563 1098,565 1102,566 1107,566 1116,566 1125,561 1129,553 L 1155,509 C 1194,527 1237,538 1282,541 L 1282,1256 C 1282,1416 1152,1547 992,1547 L 992,1547 992,1547 Z M 1335,463 L 1388,463 1388,569 1335,569 1335,463 1335,463 Z M 1441,410 L 1547,410 1547,621 1441,621 1441,410 1441,410 Z" {}
path fill=(logo_rgb) d="M 1150,1018 L 463,1018 C 448,1018 436,1030 436,1044 L 436,1177 C 436,1348 545,1468 701,1468 L 912,1468 C 1068,1468 1177,1348 1177,1177 L 1177,1044 C 1177,1030 1165,1018 1150,1018 L 1150,1018 1150,1018 Z M 912,1071 L 1018,1071 1018,1124 912,1124 912,1071 912,1071 Z M 489,1071 L 542,1071 542,1124 489,1124 489,1071 489,1071 Z M 701,1415 L 700,1415 C 701,1385 704,1352 718,1343 731,1335 759,1341 795,1359 802,1363 811,1363 818,1359 854,1341 882,1335 895,1343 909,1352 912,1385 913,1415 L 912,1415 701,1415 701,1415 701,1415 Z M 1124,1177 C 1124,1296 1061,1384 966,1408 964,1365 958,1320 922,1298 894,1281 856,1283 807,1306 757,1283 719,1281 691,1298 655,1320 649,1365 647,1408 552,1384 489,1296 489,1177 L 569,1177 C 583,1177 595,1165 595,1150 L 595,1071 859,1071 859,1150 C 859,1165 871,1177 886,1177 L 1044,1177 C 1059,1177 1071,1165 1071,1150 L 1071,1071 1124,1071 1124,1177 1124,1177 1124,1177 Z" {}
path fill=(logo_rgb) d="M 1071,648 C 998,648 939,707 939,780 939,853 998,912 1071,912 1144,912 1203,853 1203,780 1203,707 1144,648 1071,648 L 1071,648 1071,648 Z M 1071,859 C 1027,859 992,824 992,780 992,736 1027,701 1071,701 1115,701 1150,736 1150,780 1150,824 1115,859 1071,859 L 1071,859 1071,859 Z" {}
}
}
}
}
}

View file

@ -0,0 +1,35 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(BaseHandle, SmartDefault)]
pub struct Translate(L10n);
impl ComponentTrait for Translate {
fn new() -> Self {
Translate::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::With(self.l10n().escaped(cx.langid()))
}
}
impl Translate {
pub fn with(l10n: L10n) -> Self {
Translate(l10n)
}
// Translate BUILDER.
#[fn_with]
pub fn alter_l10n(&mut self, l10n: L10n) -> &mut Self {
self.0 = l10n;
self
}
// Translate GETTERS.
pub fn l10n(&self) -> &L10n {
&self.0
}
}

View file

@ -0,0 +1,168 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(SmartDefault)]
pub enum WrapperType {
#[default]
Container,
Header,
Footer,
Main,
Section,
}
#[rustfmt::skip]
#[derive(BaseHandle, ComponentClasses, SmartDefault)]
pub struct Wrapper {
id : OptionId,
weight : Weight,
renderable : Renderable,
classes : OptionClasses,
inner_classes: OptionClasses,
wrapper_type : WrapperType,
stuff : AnyComponents,
}
impl ComponentTrait for Wrapper {
fn new() -> Self {
Wrapper::default()
.with_classes(ClassesOp::Add, "container")
.with_inner_classes(ClassesOp::Add, "container")
}
fn id(&self) -> Option<String> {
self.id.get()
}
fn weight(&self) -> Weight {
self.weight
}
fn is_renderable(&self, cx: &Context) -> bool {
(self.renderable.check)(cx)
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.wrapper_type() {
WrapperType::Header => PrepareMarkup::With(html! {
header id=[self.id()] class=[self.classes().get()] {
div class=[self.inner_classes().get()] {
(self.components().render(cx))
}
}
}),
WrapperType::Footer => PrepareMarkup::With(html! {
footer id=[self.id()] class=[self.classes().get()] {
div class=[self.inner_classes().get()] {
(self.components().render(cx))
}
}
}),
WrapperType::Main => PrepareMarkup::With(html! {
main id=[self.id()] class=[self.classes().get()] {
div class=[self.inner_classes().get()] {
(self.components().render(cx))
}
}
}),
WrapperType::Section => PrepareMarkup::With(html! {
section id=[self.id()] class=[self.classes().get()] {
div class=[self.inner_classes().get()] {
(self.components().render(cx))
}
}
}),
_ => PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] {
(self.components().render(cx))
}
}),
}
}
}
impl Wrapper {
pub fn header() -> Self {
let mut c = Wrapper::default()
.with_classes(ClassesOp::Add, "header")
.with_inner_classes(ClassesOp::Add, "container");
c.wrapper_type = WrapperType::Header;
c
}
pub fn footer() -> Self {
let mut c = Wrapper::default()
.with_classes(ClassesOp::Add, "footer")
.with_inner_classes(ClassesOp::Add, "container");
c.wrapper_type = WrapperType::Footer;
c
}
pub fn main() -> Self {
let mut c = Wrapper::default()
.with_classes(ClassesOp::Add, "main")
.with_inner_classes(ClassesOp::Add, "container");
c.wrapper_type = WrapperType::Main;
c
}
pub fn section() -> Self {
let mut c = Wrapper::default()
.with_classes(ClassesOp::Add, "section")
.with_inner_classes(ClassesOp::Add, "container");
c.wrapper_type = WrapperType::Section;
c
}
// Wrapper BUILDER.
#[fn_with]
pub fn alter_id(&mut self, id: impl Into<String>) -> &mut Self {
self.id.alter_value(id);
self
}
#[fn_with]
pub fn alter_weight(&mut self, value: Weight) -> &mut Self {
self.weight = value;
self
}
#[fn_with]
pub fn alter_renderable(&mut self, check: FnIsRenderable) -> &mut Self {
self.renderable.check = check;
self
}
#[fn_with]
pub fn alter_inner_classes(&mut self, op: ClassesOp, classes: impl Into<String>) -> &mut Self {
self.inner_classes.alter_value(op, classes);
self
}
#[rustfmt::skip]
pub fn add_component(mut self, component: impl ComponentTrait) -> Self {
self.stuff.alter_value(ArcAnyOp::Add(ArcAnyComponent::new(component)));
self
}
#[fn_with]
pub fn alter_components(&mut self, op: ArcAnyOp) -> &mut Self {
self.stuff.alter_value(op);
self
}
// Wrapper GETTERS.
pub fn inner_classes(&self) -> &OptionClasses {
&self.inner_classes
}
pub fn wrapper_type(&self) -> &WrapperType {
&self.wrapper_type
}
pub fn components(&self) -> &AnyComponents {
&self.stuff
}
}

8
src/base/theme.rs Normal file
View file

@ -0,0 +1,8 @@
mod basic;
pub use basic::Basic;
mod chassis;
pub use chassis::Chassis;
mod inception;
pub use inception::Inception;

32
src/base/theme/basic.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(BaseHandle)]
pub struct Basic;
impl PackageTrait for Basic {
fn name(&self) -> L10n {
L10n::n("Basic")
}
fn theme(&self) -> Option<ThemeRef> {
Some(&Basic)
}
}
impl ThemeTrait for Basic {
fn after_prepare_body(&self, page: &mut Page) {
page.alter_favicon(Some(Favicon::new().with_icon("/base/favicon.ico")))
.alter_context(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/normalize.min.css")
.with_version("8.0.1")
.with_weight(-90),
))
.alter_context(ContextOp::AddBaseAssets)
.alter_context(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/basic.css")
.with_version("0.0.1")
.with_weight(-90),
));
}
}

32
src/base/theme/chassis.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(BaseHandle)]
pub struct Chassis;
impl PackageTrait for Chassis {
fn name(&self) -> L10n {
L10n::n("Chassis")
}
fn theme(&self) -> Option<ThemeRef> {
Some(&Chassis)
}
}
impl ThemeTrait for Chassis {
fn after_prepare_body(&self, page: &mut Page) {
page.alter_favicon(Some(Favicon::new().with_icon("/base/favicon.ico")))
.alter_context(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/normalize.min.css")
.with_version("8.0.1")
.with_weight(-90),
))
.alter_context(ContextOp::AddBaseAssets)
.alter_context(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/chassis.css")
.with_version("0.0.1")
.with_weight(-90),
));
}
}

View file

@ -0,0 +1,32 @@
use crate::prelude::*;
use crate::BaseHandle;
#[derive(BaseHandle)]
pub struct Inception;
impl PackageTrait for Inception {
fn name(&self) -> L10n {
L10n::n("Inception")
}
fn theme(&self) -> Option<ThemeRef> {
Some(&Inception)
}
}
impl ThemeTrait for Inception {
fn after_prepare_body(&self, page: &mut Page) {
page.alter_favicon(Some(Favicon::new().with_icon("/base/favicon.ico")))
.alter_context(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/normalize.min.css")
.with_version("8.0.1")
.with_weight(-90),
))
.alter_context(ContextOp::AddBaseAssets)
.alter_context(ContextOp::AddStyleSheet(
StyleSheet::at("/base/css/inception.css")
.with_version("0.0.1")
.with_weight(-90),
));
}
}

351
src/config.rs Normal file
View file

@ -0,0 +1,351 @@
//! Retrieve and apply settings values from configuration files.
//!
//! Carga la configuración de la aplicación en forma de pares `clave = valor` recogidos en archivos
//! [TOML](https://toml.io).
//!
//! La metodología [The Twelve-Factor App](https://12factor.net/es/) define **la configuración de
//! una aplicación como todo lo que puede variar entre despliegues**, diferenciando entre entornos
//! de desarrollo, pre-producción, producción, etc.
//!
//! A veces las aplicaciones guardan configuraciones como constantes en el código, lo que implica
//! una violación de esta metodología. PageTop recomienda una **estricta separación entre código y
//! configuración**. La configuración variará en cada tipo de despliegue, y el código no.
//!
//!
//! # Cómo cargar los ajustes de configuración
//!
//! Si tu aplicación requiere archivos de configuración debes crear un directorio *config* al mismo
//! nivel del archivo *Cargo.toml* de tu proyecto (o del ejecutable binario de la aplicación).
//!
//! PageTop se encargará de cargar todos los ajustes de configuración de tu aplicación leyendo los
//! siguientes archivos TOML en este orden (todos los archivos son opcionales):
//!
//! 1. **config/common.toml**, útil para los ajustes comunes a cualquier entorno. Estos valores
//! podrán ser sobrescritos al fusionar los archivos de configuración restantes.
//!
//! 2. **config/{file}.toml**, donde *{file}* se define con la variable de entorno
//! `PAGETOP_RUN_MODE`:
//!
//! * Si no está definida se asumirá *default* por defecto y PageTop intentará cargar el archivo
//! *config/default.toml* si existe.
//!
//! * De esta manera podrás tener diferentes ajustes de configuración para diferentes entornos
//! de ejecución. Por ejemplo, para *devel.toml*, *staging.toml* o *production.toml*. O
//! también para *server1.toml* o *server2.toml*. Sólo uno será cargado.
//!
//! * Normalmente estos archivos suelen ser idóneos para incluir contraseñas o configuración
//! sensible asociada al entorno correspondiente. Estos archivos no deberían ser publicados en
//! el repositorio Git por razones de seguridad.
//!
//! 3. **config/local.toml**, para añadir o sobrescribir ajustes de los archivos anteriores.
//!
//!
//! # Cómo añadir ajustes de configuración
//!
//! Para proporcionar a tu **módulo** sus propios ajustes de configuración, añade
//! [*serde*](https://docs.rs/serde) en las dependencias de tu archivo *Cargo.toml* habilitando la
//! característica `derive`:
//!
//! ```toml
//! [dependencies]
//! serde = { version = "1.0", features = ["derive"] }
//! ```
//!
//! Y luego inicializa con la macro [`default_settings!`](crate::default_settings) tus ajustes,
//! usando tipos seguros y asignando los valores predefinidos para la estructura asociada:
//!
//! ```
//! use pagetop::prelude::*;
//! use serde::Deserialize;
//!
//! #[derive(Debug, Deserialize)]
//! pub struct Settings {
//! pub myapp: MyApp,
//! }
//!
//! #[derive(Debug, Deserialize)]
//! pub struct MyApp {
//! pub name: String,
//! pub description: Option<String>,
//! pub width: u16,
//! pub height: u16,
//! }
//!
//! default_settings!(
//! // [myapp]
//! "myapp.name" => "Value Name",
//! "myapp.width" => 900,
//! "myapp.height" => 320,
//! );
//! ```
//!
//! De hecho, así se declaran los ajustes globales de la configuración (ver [`SETTINGS`]).
//!
//! Puedes usar la [sintaxis TOML](https://toml.io/en/v1.0.0#table) para añadir tu nueva sección
//! `[myapp]` en los archivos de configuración, del mismo modo que se añaden `[log]` o `[server]` en
//! los ajustes globales (ver [`Settings`]).
//!
//! Se recomienda inicializar todos los ajustes con valores predefinidos, o utilizar la notación
//! `Option<T>` si van a ser tratados en el código como opcionales.
//!
//! Si no pueden inicializarse correctamente los ajustes de configuración, entonces la aplicación
//! ejecutará un panic! y detendrá la ejecución.
//!
//! Los ajustes de configuración siempre serán de sólo lectura.
//!
//!
//! # Cómo usar tus nuevos ajustes de configuración
//!
//! ```
//! use pagetop::prelude::*;
//!
//! fn global_settings() {
//! println!("App name: {}", &config::SETTINGS.app.name);
//! println!("App description: {}", &config::SETTINGS.app.description);
//! println!("Value of PAGETOP_RUN_MODE: {}", &config::SETTINGS.app.run_mode);
//! }
//!
//! fn package_settings() {
//! println!("{} - {:?}", &SETTINGS.myapp.name, &SETTINGS.myapp.description);
//! println!("{}", &SETTINGS.myapp.width);
//! }
//! ```
mod data;
mod de;
mod error;
mod file;
mod path;
mod source;
mod value;
use crate::config::data::ConfigData;
use crate::config::file::File;
use crate::{concat_string, LazyStatic};
use serde::Deserialize;
use std::env;
/// Directorio donde se encuentran los archivos de configuración.
const CONFIG_DIR: &str = "config";
/// Valores originales de la configuración en forma de pares `clave = valor` recogidos de los
/// archivos de configuración.
#[rustfmt::skip]
pub static CONFIG: LazyStatic<ConfigData> = LazyStatic::new(|| {
// Modo de ejecución según la variable de entorno PAGETOP_RUN_MODE. Por defecto 'default'.
let run_mode = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into());
// Inicializa los ajustes.
let mut settings = ConfigData::default();
// Combina los archivos (opcionales) de configuración y asigna el modo de ejecución.
settings
// Primero añade la configuración común a todos los entornos. Por defecto 'common.toml'.
.merge(
File::with_name(&concat_string!(CONFIG_DIR, "/common.toml"))
.required(false)
).unwrap()
// Añade la configuración específica del entorno. Por defecto 'default.toml'.
.merge(
File::with_name(&concat_string!(CONFIG_DIR, "/", run_mode, ".toml"))
.required(false)
).unwrap()
// Añade la configuración local reservada del entorno. Por defecto 'default.local.toml'.
.merge(
File::with_name(&concat_string!(CONFIG_DIR, "/local.", run_mode, ".toml"))
.required(false),
).unwrap()
// Añade la configuración local reservada general. Por defecto 'local.toml'.
.merge(
File::with_name(&concat_string!(CONFIG_DIR, "/local.toml"))
.required(false)
).unwrap()
// Salvaguarda el modo de ejecución.
.set("app.run_mode", run_mode)
.unwrap();
settings
});
#[macro_export]
/// Define un conjunto de ajustes de configuración usando tipos seguros y valores predefinidos.
///
/// Detiene la aplicación con un panic! si no pueden asignarse los ajustes de configuración.
///
/// Ver [`Cómo añadir ajustes de configuración`](config/index.html#cómo-añadir-ajustes-de-configuración).
macro_rules! default_settings {
( $($key:literal => $value:literal),* $(,)? ) => {
#[doc = concat!(
"Assigned or predefined values for configuration settings associated to the ",
"[`Settings`] type."
)]
pub static SETTINGS: $crate::LazyStatic<Settings> = $crate::LazyStatic::new(|| {
let mut settings = $crate::config::CONFIG.clone();
$(
settings.set_default($key, $value).unwrap();
)*
match settings.try_into() {
Ok(s) => s,
Err(e) => panic!("Error parsing settings: {}", e),
}
});
};
}
#[derive(Debug, Deserialize)]
/// Configuration settings for the [`[app]`](App), [`[database]`](Database), [`[dev]`](Dev),
/// [`[log]`](Log), and [`[server]`](Server) sections (see [`SETTINGS`]).
pub struct Settings {
pub app: App,
pub database: Database,
pub dev: Dev,
pub log: Log,
pub server: Server,
}
#[derive(Debug, Deserialize)]
/// Section `[app]` of the configuration settings.
///
/// See [`Settings`].
pub struct App {
/// El nombre de la aplicación.
/// Por defecto: *"PageTop App"*.
pub name: String,
/// Una descripción breve de la aplicación.
/// Por defecto: *"Developed with the awesome PageTop framework."*.
pub description: String,
/// Tema predeterminado.
/// Por defecto: *"Default"*.
pub theme: String,
/// Idioma (localización) predeterminado.
/// Por defecto: *"en-US"*.
pub language: String,
/// Dirección predeterminada para el texto: *"ltr"* (de izquierda a derecha), *"rtl"* (de
/// derecha a izquierda) o *"auto"*.
/// Por defecto: *"ltr"*.
pub direction: String,
/// Rótulo de texto ASCII al arrancar: *"Off"*, *"Slant"*, *"Small"*, *"Speed"* o *"Starwars"*.
/// Por defecto: *"Slant"*.
pub startup_banner: String,
/// Por defecto: según variable de entorno `PAGETOP_RUN_MODE`, o *"default"* si no lo está.
pub run_mode: String,
}
#[derive(Debug, Deserialize)]
/// Section `[database]` of the configuration settings.
///
/// See [`Settings`].
pub struct Database {
/// Tipo de base de datos: *"mysql"*, *"postgres"* ó *"sqlite"*.
/// Por defecto: *""*.
pub db_type: String,
/// Nombre (para mysql/postgres) o referencia (para sqlite) de la base de datos.
/// Por defecto: *""*.
pub db_name: String,
/// Usuario de conexión a la base de datos (para mysql/postgres).
/// Por defecto: *""*.
pub db_user: String,
/// Contraseña para la conexión a la base de datos (para mysql/postgres).
/// Por defecto: *""*.
pub db_pass: String,
/// Servidor de conexión a la base de datos (para mysql/postgres).
/// Por defecto: *"localhost"*.
pub db_host: String,
/// Puerto de conexión a la base de datos, normalmente 3306 (para mysql) ó 5432 (para postgres).
/// Por defecto: *0*.
pub db_port: u16,
/// Número máximo de conexiones habilitadas.
/// Por defecto: *5*.
pub max_pool_size: u32,
}
#[derive(Debug, Deserialize)]
/// Section `[dev]` of the configuration settings.
///
/// See [`Settings`].
pub struct Dev {
/// Los archivos estáticos requeridos por la aplicación se integran de manera predeterminada en
/// el binario ejecutable. Sin embargo, durante el desarrollo puede resultar útil servir estos
/// archivos desde su propio directorio para evitar recompilar cada vez que se modifican. En
/// este caso bastaría con indicar la ruta completa al directorio raíz del proyecto.
/// Por defecto: *""*.
pub pagetop_project_dir: String,
}
#[derive(Debug, Deserialize)]
/// Section `[log]` of the configuration settings.
///
/// See [`Settings`].
pub struct Log {
/// Filtro, o combinación de filtros separados por coma, para la traza de ejecución: *"Error"*,
/// *"Warn"*, *"Info"*, *"Debug"* o *"Trace"*.
/// Por ejemplo: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
/// Por defecto: *"Info"*.
pub tracing: String,
/// Muestra la traza en el terminal (*"Stdout"*) o queda registrada en archivos con rotación
/// *"Daily"*, *"Hourly"*, *"Minutely"* o *"Endless"*.
/// Por defecto: *"Stdout"*.
pub rolling: String,
/// Directorio para los archivos de traza (si `rolling` != *"Stdout"*).
/// Por defecto: *"log"*.
pub path: String,
/// Prefijo para los archivos de traza (si `rolling` != *"Stdout"*).
/// Por defecto: *"tracing.log"*.
pub prefix: String,
/// Presentación de las trazas. Puede ser *"Full"*, *"Compact"*, *"Pretty"* o *"Json"*.
/// Por defecto: *"Full"*.
pub format: String,
}
#[derive(Debug, Deserialize)]
/// Section `[server]` of the configuration settings.
///
/// See [`Settings`].
pub struct Server {
/// Dirección del servidor web.
/// Por defecto: *"localhost"*.
pub bind_address: String,
/// Puerto del servidor web.
/// Por defecto: *8088*.
pub bind_port: u16,
/// Duración en segundos para la sesión (0 indica "hasta que se cierre el navegador").
/// Por defecto: *604800* (7 días).
pub session_lifetime: i64,
}
default_settings!(
// [app]
"app.name" => "PageTop App",
"app.description" => "Developed with the awesome PageTop framework.",
"app.theme" => "Default",
"app.language" => "en-US",
"app.direction" => "ltr",
"app.startup_banner" => "Slant",
// [database]
"database.db_type" => "",
"database.db_name" => "",
"database.db_user" => "",
"database.db_pass" => "",
"database.db_host" => "localhost",
"database.db_port" => 0,
"database.max_pool_size" => 5,
// [dev]
"dev.pagetop_project_dir" => "",
// [log]
"log.tracing" => "Info",
"log.rolling" => "Stdout",
"log.path" => "log",
"log.prefix" => "tracing.log",
"log.format" => "Full",
// [server]
"server.bind_address" => "localhost",
"server.bind_port" => 8088,
"server.session_lifetime" => 604800,
);

136
src/config/data.rs Normal file
View file

@ -0,0 +1,136 @@
use crate::config::error::*;
use crate::config::path;
use crate::config::source::Source;
use crate::config::value::Value;
use serde::de::Deserialize;
use std::collections::HashMap;
use std::fmt::Debug;
#[derive(Clone, Debug)]
enum ConfigKind {
// A mutable configuration. This is the default.
Mutable {
defaults: HashMap<path::Expression, Value>,
overrides: HashMap<path::Expression, Value>,
sources: Vec<Box<dyn Source + Send + Sync>>,
},
}
impl Default for ConfigKind {
fn default() -> Self {
ConfigKind::Mutable {
defaults: HashMap::new(),
overrides: HashMap::new(),
sources: Vec::new(),
}
}
}
/// A prioritized configuration repository. It maintains a set of configuration sources, fetches
/// values to populate those, and provides them according to the source's priority.
#[derive(Default, Clone, Debug)]
pub struct ConfigData {
kind: ConfigKind,
/// Root of the cached configuration.
pub cache: Value,
}
impl ConfigData {
/// Merge in a configuration property source.
pub fn merge<T>(&mut self, source: T) -> Result<&mut ConfigData>
where
T: 'static,
T: Source + Send + Sync,
{
match self.kind {
ConfigKind::Mutable {
ref mut sources, ..
} => {
sources.push(Box::new(source));
}
}
self.refresh()
}
/// Refresh the configuration cache with fresh data from added sources.
///
/// Configuration is automatically refreshed after a mutation operation (`set`, `merge`,
/// `set_default`, etc.).
pub fn refresh(&mut self) -> Result<&mut ConfigData> {
self.cache = match self.kind {
// TODO: We need to actually merge in all the stuff.
ConfigKind::Mutable {
ref overrides,
ref sources,
ref defaults,
} => {
let mut cache: Value = HashMap::<String, Value>::new().into();
// Add defaults.
for (key, val) in defaults {
key.set(&mut cache, val.clone());
}
// Add sources.
sources.collect_to(&mut cache)?;
// Add overrides.
for (key, val) in overrides {
key.set(&mut cache, val.clone());
}
cache
}
};
Ok(self)
}
pub fn set_default<T>(&mut self, key: &str, value: T) -> Result<&mut ConfigData>
where
T: Into<Value>,
{
match self.kind {
ConfigKind::Mutable {
ref mut defaults, ..
} => {
defaults.insert(key.parse()?, value.into());
}
};
self.refresh()
}
pub fn set<T>(&mut self, key: &str, value: T) -> Result<&mut ConfigData>
where
T: Into<Value>,
{
match self.kind {
ConfigKind::Mutable {
ref mut overrides, ..
} => {
overrides.insert(key.parse()?, value.into());
}
};
self.refresh()
}
/// Attempt to deserialize the entire configuration into the requested type.
pub fn try_into<'de, T: Deserialize<'de>>(self) -> Result<T> {
T::deserialize(self)
}
}
impl Source for ConfigData {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<HashMap<String, Value>> {
self.cache.clone().into_table()
}
}

462
src/config/de.rs Normal file
View file

@ -0,0 +1,462 @@
use crate::config::data::ConfigData;
use crate::config::error::*;
use crate::config::value::{Table, Value, ValueKind};
use serde::de;
use serde::forward_to_deserialize_any;
use std::collections::{HashMap, VecDeque};
use std::iter::Enumerate;
impl<'de> de::Deserializer<'de> for Value {
type Error = ConfigError;
#[inline]
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
// Deserialize based on the underlying type.
match self.kind {
ValueKind::Nil => visitor.visit_unit(),
ValueKind::Integer(i) => visitor.visit_i64(i),
ValueKind::Boolean(b) => visitor.visit_bool(b),
ValueKind::Float(f) => visitor.visit_f64(f),
ValueKind::String(s) => visitor.visit_string(s),
ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)),
ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)),
}
}
#[inline]
fn deserialize_bool<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_bool(self.into_bool()?)
}
#[inline]
fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i8(self.into_int()? as i8)
}
#[inline]
fn deserialize_i16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i16(self.into_int()? as i16)
}
#[inline]
fn deserialize_i32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i32(self.into_int()? as i32)
}
#[inline]
fn deserialize_i64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_i64(self.into_int()?)
}
#[inline]
fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u8(self.into_int()? as u8)
}
#[inline]
fn deserialize_u16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u16(self.into_int()? as u16)
}
#[inline]
fn deserialize_u32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u32(self.into_int()? as u32)
}
#[inline]
fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u64(self.into_int()? as u64)
}
#[inline]
fn deserialize_f32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_f32(self.into_float()? as f32)
}
#[inline]
fn deserialize_f64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_f64(self.into_float()?)
}
#[inline]
fn deserialize_str<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_string(self.into_str()?)
}
#[inline]
fn deserialize_string<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_string(self.into_str()?)
}
#[inline]
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
// Match an explicit nil as None and everything else as Some.
match self.kind {
ValueKind::Nil => visitor.visit_none(),
_ => visitor.visit_some(self),
}
}
fn deserialize_newtype_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
visitor.visit_newtype_struct(self)
}
fn deserialize_enum<V>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
visitor.visit_enum(EnumAccess {
value: self,
name,
variants,
})
}
forward_to_deserialize_any! {
char seq
bytes byte_buf map struct unit
identifier ignored_any unit_struct tuple_struct tuple
}
}
struct StrDeserializer<'a>(&'a str);
impl<'de, 'a> de::Deserializer<'de> for StrDeserializer<'a> {
type Error = ConfigError;
#[inline]
fn deserialize_any<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_str(self.0)
}
forward_to_deserialize_any! {
bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
bytes byte_buf map struct unit enum newtype_struct
identifier ignored_any unit_struct tuple_struct tuple option
}
}
struct SeqAccess {
elements: Enumerate<::std::vec::IntoIter<Value>>,
}
impl SeqAccess {
fn new(elements: Vec<Value>) -> Self {
SeqAccess {
elements: elements.into_iter().enumerate(),
}
}
}
impl<'de> de::SeqAccess<'de> for SeqAccess {
type Error = ConfigError;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
where
T: de::DeserializeSeed<'de>,
{
match self.elements.next() {
Some((idx, value)) => seed
.deserialize(value)
.map(Some)
.map_err(|e| e.prepend_index(idx)),
None => Ok(None),
}
}
fn size_hint(&self) -> Option<usize> {
match self.elements.size_hint() {
(lower, Some(upper)) if lower == upper => Some(upper),
_ => None,
}
}
}
struct MapAccess {
elements: VecDeque<(String, Value)>,
}
impl MapAccess {
fn new(table: HashMap<String, Value>) -> Self {
MapAccess {
elements: table.into_iter().collect(),
}
}
}
impl<'de> de::MapAccess<'de> for MapAccess {
type Error = ConfigError;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
where
K: de::DeserializeSeed<'de>,
{
if let Some((key_s, _)) = self.elements.front() {
let key_de = Value::new(None, key_s as &str);
let key = de::DeserializeSeed::deserialize(seed, key_de)?;
Ok(Some(key))
} else {
Ok(None)
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
where
V: de::DeserializeSeed<'de>,
{
let (key, value) = self.elements.pop_front().unwrap();
de::DeserializeSeed::deserialize(seed, value).map_err(|e| e.prepend_key(key))
}
}
struct EnumAccess {
value: Value,
name: &'static str,
variants: &'static [&'static str],
}
impl EnumAccess {
fn variant_deserializer(&self, name: &str) -> Result<StrDeserializer> {
self.variants
.iter()
.find(|&&s| s == name)
.map(|&s| StrDeserializer(s))
.ok_or_else(|| self.no_constructor_error(name))
}
fn table_deserializer(&self, table: &Table) -> Result<StrDeserializer> {
if table.len() == 1 {
self.variant_deserializer(table.iter().next().unwrap().0)
} else {
Err(self.structural_error())
}
}
fn no_constructor_error(&self, supposed_variant: &str) -> ConfigError {
ConfigError::Message(format!(
"enum {} does not have variant constructor {}",
self.name, supposed_variant
))
}
fn structural_error(&self) -> ConfigError {
ConfigError::Message(format!(
"value of enum {} should be represented by either string or table with exactly one key",
self.name
))
}
}
impl<'de> de::EnumAccess<'de> for EnumAccess {
type Error = ConfigError;
type Variant = Self;
fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant)>
where
V: de::DeserializeSeed<'de>,
{
let value = {
let deserializer = match self.value.kind {
ValueKind::String(ref s) => self.variant_deserializer(s),
ValueKind::Table(ref t) => self.table_deserializer(t),
_ => Err(self.structural_error()),
}?;
seed.deserialize(deserializer)?
};
Ok((value, self))
}
}
impl<'de> de::VariantAccess<'de> for EnumAccess {
type Error = ConfigError;
fn unit_variant(self) -> Result<()> {
Ok(())
}
fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value>
where
T: de::DeserializeSeed<'de>,
{
match self.value.kind {
ValueKind::Table(t) => seed.deserialize(t.into_iter().next().unwrap().1),
_ => unreachable!(),
}
}
fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
match self.value.kind {
ValueKind::Table(t) => {
de::Deserializer::deserialize_seq(t.into_iter().next().unwrap().1, visitor)
}
_ => unreachable!(),
}
}
fn struct_variant<V>(self, _fields: &'static [&'static str], visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
match self.value.kind {
ValueKind::Table(t) => {
de::Deserializer::deserialize_map(t.into_iter().next().unwrap().1, visitor)
}
_ => unreachable!(),
}
}
}
impl<'de> de::Deserializer<'de> for ConfigData {
type Error = ConfigError;
#[inline]
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
// Deserialize based on the underlying type.
match self.cache.kind {
ValueKind::Nil => visitor.visit_unit(),
ValueKind::Integer(i) => visitor.visit_i64(i),
ValueKind::Boolean(b) => visitor.visit_bool(b),
ValueKind::Float(f) => visitor.visit_f64(f),
ValueKind::String(s) => visitor.visit_string(s),
ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)),
ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)),
}
}
#[inline]
fn deserialize_bool<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_bool(self.cache.into_bool()?)
}
#[inline]
fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i8(self.cache.into_int()? as i8)
}
#[inline]
fn deserialize_i16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i16(self.cache.into_int()? as i16)
}
#[inline]
fn deserialize_i32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_i32(self.cache.into_int()? as i32)
}
#[inline]
fn deserialize_i64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_i64(self.cache.into_int()?)
}
#[inline]
fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u8(self.cache.into_int()? as u8)
}
#[inline]
fn deserialize_u16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u16(self.cache.into_int()? as u16)
}
#[inline]
fn deserialize_u32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u32(self.cache.into_int()? as u32)
}
#[inline]
fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
// FIXME: This should *fail* if the value does not fit in the requets integer type.
visitor.visit_u64(self.cache.into_int()? as u64)
}
#[inline]
fn deserialize_f32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_f32(self.cache.into_float()? as f32)
}
#[inline]
fn deserialize_f64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_f64(self.cache.into_float()?)
}
#[inline]
fn deserialize_str<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_string(self.cache.into_str()?)
}
#[inline]
fn deserialize_string<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
visitor.visit_string(self.cache.into_str()?)
}
#[inline]
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
// Match an explicit nil as None and everything else as Some.
match self.cache.kind {
ValueKind::Nil => visitor.visit_none(),
_ => visitor.visit_some(self),
}
}
fn deserialize_enum<V>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value>
where
V: de::Visitor<'de>,
{
visitor.visit_enum(EnumAccess {
value: self.cache,
name,
variants,
})
}
forward_to_deserialize_any! {
char seq
bytes byte_buf map struct unit newtype_struct
identifier ignored_any unit_struct tuple_struct tuple
}
}

222
src/config/error.rs Normal file
View file

@ -0,0 +1,222 @@
use nom;
use serde::de;
use serde::ser;
use std::error::Error;
use std::fmt;
use std::result;
#[derive(Debug)]
pub enum Unexpected {
Bool(bool),
Integer(i64),
Float(f64),
Str(String),
Unit,
Seq,
Map,
}
impl fmt::Display for Unexpected {
fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
match *self {
Unexpected::Bool(b) => write!(f, "boolean `{}`", b),
Unexpected::Integer(i) => write!(f, "integer `{}`", i),
Unexpected::Float(v) => write!(f, "floating point `{}`", v),
Unexpected::Str(ref s) => write!(f, "string {:?}", s),
Unexpected::Unit => write!(f, "unit value"),
Unexpected::Seq => write!(f, "sequence"),
Unexpected::Map => write!(f, "map"),
}
}
}
/// Represents all possible errors that can occur when working with configuration.
pub enum ConfigError {
/// Configuration is frozen and no further mutations can be made.
Frozen,
/// Configuration property was not found.
NotFound(String),
/// Configuration path could not be parsed.
PathParse(nom::error::ErrorKind),
/// Configuration could not be parsed from file.
FileParse {
/// The URI used to access the file (if not loaded from a string).
/// Example: `/path/to/config.json`
uri: Option<String>,
/// The captured error from attempting to parse the file in its desired format.
/// This is the actual error object from the library used for the parsing.
cause: Box<dyn Error + Send + Sync>,
},
/// Value could not be converted into the requested type.
Type {
/// The URI that references the source that the value came from.
/// Example: `/path/to/config.json` or `Environment` or `etcd://localhost`
// TODO: Why is this called Origin but FileParse has a uri field?
origin: Option<String>,
/// What we found when parsing the value.
unexpected: Unexpected,
/// What was expected when parsing the value.
expected: &'static str,
/// The key in the configuration hash of this value (if available where the error is
/// generated).
key: Option<String>,
},
/// Custom message.
Message(String),
/// Unadorned error from a foreign origin.
Foreign(Box<dyn Error + Send + Sync>),
}
impl ConfigError {
// FIXME: pub(crate).
#[doc(hidden)]
pub fn invalid_type(
origin: Option<String>,
unexpected: Unexpected,
expected: &'static str,
) -> Self {
ConfigError::Type {
origin,
unexpected,
expected,
key: None,
}
}
// FIXME: pub(crate).
#[doc(hidden)]
pub fn extend_with_key(self, key: &str) -> Self {
match self {
ConfigError::Type {
origin,
unexpected,
expected,
..
} => ConfigError::Type {
origin,
unexpected,
expected,
key: Some(key.into()),
},
_ => self,
}
}
fn prepend(self, segment: String, add_dot: bool) -> Self {
let concat = |key: Option<String>| {
let key = key.unwrap_or_default();
let dot = if add_dot && key.as_bytes().first().unwrap_or(&b'[') != &b'[' {
"."
} else {
""
};
format!("{}{}{}", segment, dot, key)
};
match self {
ConfigError::Type {
origin,
unexpected,
expected,
key,
} => ConfigError::Type {
origin,
unexpected,
expected,
key: Some(concat(key)),
},
ConfigError::NotFound(key) => ConfigError::NotFound(concat(Some(key))),
_ => self,
}
}
pub(crate) fn prepend_key(self, key: String) -> Self {
self.prepend(key, true)
}
pub(crate) fn prepend_index(self, idx: usize) -> Self {
self.prepend(format!("[{}]", idx), false)
}
}
/// Alias for a `Result` with the error type set to `ConfigError`.
pub type Result<T> = result::Result<T, ConfigError>;
// Forward Debug to Display for readable panic! messages.
impl fmt::Debug for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", *self)
}
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ConfigError::Frozen => write!(f, "configuration is frozen"),
ConfigError::PathParse(ref kind) => write!(f, "{}", kind.description()),
ConfigError::Message(ref s) => write!(f, "{}", s),
ConfigError::Foreign(ref cause) => write!(f, "{}", cause),
ConfigError::NotFound(ref key) => {
write!(f, "configuration property {:?} not found", key)
}
ConfigError::Type {
ref origin,
ref unexpected,
expected,
ref key,
} => {
write!(f, "invalid type: {}, expected {}", unexpected, expected)?;
if let Some(ref key) = *key {
write!(f, " for key `{}`", key)?;
}
if let Some(ref origin) = *origin {
write!(f, " in {}", origin)?;
}
Ok(())
}
ConfigError::FileParse { ref cause, ref uri } => {
write!(f, "{}", cause)?;
if let Some(ref uri) = *uri {
write!(f, " in {}", uri)?;
}
Ok(())
}
}
}
}
impl Error for ConfigError {}
impl de::Error for ConfigError {
fn custom<T: fmt::Display>(msg: T) -> Self {
ConfigError::Message(msg.to_string())
}
}
impl ser::Error for ConfigError {
fn custom<T: fmt::Display>(msg: T) -> Self {
ConfigError::Message(msg.to_string())
}
}

89
src/config/file.rs Normal file
View file

@ -0,0 +1,89 @@
mod source;
mod toml;
use crate::config::error::*;
use crate::config::source::Source;
use crate::config::value::Value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use self::source::FileSource;
#[derive(Clone, Debug)]
pub struct File<T>
where
T: FileSource,
{
source: T,
/// A required File will error if it cannot be found.
required: bool,
}
impl File<source::FileSourceFile> {
/// Given the basename of a file, will attempt to locate a file by setting its extension to a
/// registered format.
pub fn with_name(name: &str) -> Self {
File {
source: source::FileSourceFile::new(name.into()),
required: true,
}
}
}
impl<'a> From<&'a Path> for File<source::FileSourceFile> {
fn from(path: &'a Path) -> Self {
File {
source: source::FileSourceFile::new(path.to_path_buf()),
required: true,
}
}
}
impl From<PathBuf> for File<source::FileSourceFile> {
fn from(path: PathBuf) -> Self {
File {
source: source::FileSourceFile::new(path),
required: true,
}
}
}
impl<T: FileSource> File<T> {
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
}
impl<T: FileSource> Source for File<T>
where
T: 'static,
T: Sync + Send,
{
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<HashMap<String, Value>> {
// Coerce the file contents to a string.
let (uri, contents) = match self
.source
.resolve()
.map_err(|err| ConfigError::Foreign(err))
{
Ok((uri, contents)) => (uri, contents),
Err(error) => {
if !self.required {
return Ok(HashMap::new());
}
return Err(error);
}
};
// Parse the string using the given format.
toml::parse(uri.as_ref(), &contents).map_err(|cause| ConfigError::FileParse { uri, cause })
}
}

126
src/config/file/source.rs Normal file
View file

@ -0,0 +1,126 @@
use std::env;
use std::error::Error;
use std::fmt::Debug;
use std::fs;
use std::io::{self, Read};
use std::iter::Iterator;
use std::path::{Path, PathBuf};
/// Describes where the file is sourced.
pub trait FileSource: Debug + Clone {
fn resolve(&self) -> Result<(Option<String>, String), Box<dyn Error + Send + Sync>>;
}
/// Describes a file sourced from a file.
#[derive(Clone, Debug)]
pub struct FileSourceFile {
/// Path of configuration file.
name: PathBuf,
}
impl FileSourceFile {
pub fn new(name: PathBuf) -> FileSourceFile {
FileSourceFile { name }
}
fn find_file(&self) -> Result<PathBuf, Box<dyn Error + Send + Sync>> {
// First check for an _exact_ match.
let mut filename = env::current_dir()?.as_path().join(self.name.clone());
if filename.is_file() {
if vec!["toml"].contains(
&filename
.extension()
.unwrap_or_default()
.to_string_lossy()
.as_ref(),
) {
return Ok(filename);
}
Err(Box::new(io::Error::new(
io::ErrorKind::NotFound,
format!(
"configuration file \"{}\" is not of a registered file format",
filename.to_string_lossy()
),
)))
} else {
filename.set_extension("toml");
if filename.is_file() {
return Ok(filename);
}
Err(Box::new(io::Error::new(
io::ErrorKind::NotFound,
format!(
"configuration file \"{}\" not found",
self.name.to_string_lossy()
),
)))
}
}
}
impl FileSource for FileSourceFile {
fn resolve(&self) -> Result<(Option<String>, String), Box<dyn Error + Send + Sync>> {
// Find file.
let filename = self.find_file()?;
// Attempt to use a relative path for the URI.
let base = env::current_dir()?;
let uri = match path_relative_from(&filename, &base) {
Some(value) => value,
None => filename.clone(),
};
// Read contents from file.
let mut file = fs::File::open(filename)?;
let mut text = String::new();
file.read_to_string(&mut text)?;
Ok((Some(uri.to_string_lossy().into_owned()), text))
}
}
// TODO: This should probably be a crate.
// https://github.com/rust-lang/rust/blob/master/src/librustc_trans/back/rpath.rs#L128
fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> {
use std::path::Component;
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
Some(PathBuf::from(path))
} else {
None
}
} else {
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
(Some(_), Some(b)) if b == Component::ParentDir => return None,
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Some(comps.iter().map(|c| c.as_os_str()).collect())
}
}

51
src/config/file/toml.rs Normal file
View file

@ -0,0 +1,51 @@
use crate::config::value::{Value, ValueKind};
use toml;
use std::collections::HashMap;
use std::error::Error;
pub fn parse(
uri: Option<&String>,
text: &str,
) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> {
// Parse a TOML value from the provided text.
// TODO: Have a proper error fire if the root of a file is ever not a Table
let value = from_toml_value(uri, &toml::from_str(text)?);
match value.kind {
ValueKind::Table(map) => Ok(map),
_ => Ok(HashMap::new()),
}
}
fn from_toml_value(uri: Option<&String>, value: &toml::Value) -> Value {
match *value {
toml::Value::String(ref value) => Value::new(uri, value.to_string()),
toml::Value::Float(value) => Value::new(uri, value),
toml::Value::Integer(value) => Value::new(uri, value),
toml::Value::Boolean(value) => Value::new(uri, value),
toml::Value::Table(ref table) => {
let mut m = HashMap::new();
for (key, value) in table {
m.insert(key.clone(), from_toml_value(uri, value));
}
Value::new(uri, m)
}
toml::Value::Array(ref array) => {
let mut l = Vec::new();
for value in array {
l.push(from_toml_value(uri, value));
}
Value::new(uri, l)
}
toml::Value::Datetime(ref datetime) => Value::new(uri, datetime.to_string()),
}
}

167
src/config/path.rs Normal file
View file

@ -0,0 +1,167 @@
use crate::config::error::*;
use crate::config::value::{Value, ValueKind};
use std::collections::HashMap;
use std::str::FromStr;
mod parser;
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Expression {
Identifier(String),
Child(Box<Expression>, String),
Subscript(Box<Expression>, isize),
}
impl FromStr for Expression {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Expression> {
parser::from_str(s).map_err(ConfigError::PathParse)
}
}
fn sindex_to_uindex(index: isize, len: usize) -> usize {
if index >= 0 {
index as usize
} else {
len - (index.unsigned_abs())
}
}
impl Expression {
pub fn get_mut_forcibly<'a>(&self, root: &'a mut Value) -> Option<&'a mut Value> {
match *self {
Expression::Identifier(ref id) => match root.kind {
ValueKind::Table(ref mut map) => Some(
map.entry(id.clone())
.or_insert_with(|| Value::new(None, ValueKind::Nil)),
),
_ => None,
},
Expression::Child(ref expr, ref key) => match expr.get_mut_forcibly(root) {
Some(value) => match value.kind {
ValueKind::Table(ref mut map) => Some(
map.entry(key.clone())
.or_insert_with(|| Value::new(None, ValueKind::Nil)),
),
_ => {
*value = HashMap::<String, Value>::new().into();
if let ValueKind::Table(ref mut map) = value.kind {
Some(
map.entry(key.clone())
.or_insert_with(|| Value::new(None, ValueKind::Nil)),
)
} else {
unreachable!();
}
}
},
_ => None,
},
Expression::Subscript(ref expr, index) => match expr.get_mut_forcibly(root) {
Some(value) => {
match value.kind {
ValueKind::Array(_) => (),
_ => *value = Vec::<Value>::new().into(),
}
match value.kind {
ValueKind::Array(ref mut array) => {
let index = sindex_to_uindex(index, array.len());
if index >= array.len() {
array.resize(index + 1, Value::new(None, ValueKind::Nil));
}
Some(&mut array[index])
}
_ => None,
}
}
_ => None,
},
}
}
pub fn set(&self, root: &mut Value, value: Value) {
match *self {
Expression::Identifier(ref id) => {
// Ensure that root is a table.
match root.kind {
ValueKind::Table(_) => {}
_ => {
*root = HashMap::<String, Value>::new().into();
}
}
match value.kind {
ValueKind::Table(ref incoming_map) => {
// Pull out another table.
let target = if let ValueKind::Table(ref mut map) = root.kind {
map.entry(id.clone())
.or_insert_with(|| HashMap::<String, Value>::new().into())
} else {
unreachable!();
};
// Continue the deep merge.
for (key, val) in incoming_map {
Expression::Identifier(key.clone()).set(target, val.clone());
}
}
_ => {
if let ValueKind::Table(ref mut map) = root.kind {
// Just do a simple set.
map.insert(id.clone(), value);
}
}
}
}
Expression::Child(ref expr, ref key) => {
if let Some(parent) = expr.get_mut_forcibly(root) {
match parent.kind {
ValueKind::Table(_) => {
Expression::Identifier(key.clone()).set(parent, value);
}
_ => {
// Didn't find a table. Oh well. Make a table and do this anyway.
*parent = HashMap::<String, Value>::new().into();
Expression::Identifier(key.clone()).set(parent, value);
}
}
}
}
Expression::Subscript(ref expr, index) => {
if let Some(parent) = expr.get_mut_forcibly(root) {
match parent.kind {
ValueKind::Array(_) => (),
_ => *parent = Vec::<Value>::new().into(),
}
if let ValueKind::Array(ref mut array) = parent.kind {
let uindex = sindex_to_uindex(index, array.len());
if uindex >= array.len() {
array.resize(uindex + 1, Value::new(None, ValueKind::Nil));
}
array[uindex] = value;
}
}
}
}
}
}

131
src/config/path/parser.rs Normal file
View file

@ -0,0 +1,131 @@
use super::Expression;
use nom::{
branch::alt,
bytes::complete::{is_a, tag},
character::complete::{char, digit1, space0},
combinator::{map, map_res, opt, recognize},
error::ErrorKind,
sequence::{delimited, pair, preceded},
Err, IResult,
};
use std::str::FromStr;
fn raw_ident(i: &str) -> IResult<&str, String> {
map(
is_a(
"abcdefghijklmnopqrstuvwxyz \
ABCDEFGHIJKLMNOPQRSTUVWXYZ \
0123456789 \
_-",
),
|s: &str| s.to_string(),
)(i)
}
fn integer(i: &str) -> IResult<&str, isize> {
map_res(
delimited(space0, recognize(pair(opt(tag("-")), digit1)), space0),
FromStr::from_str,
)(i)
}
fn ident(i: &str) -> IResult<&str, Expression> {
map(raw_ident, Expression::Identifier)(i)
}
fn postfix<'a>(expr: Expression) -> impl FnMut(&'a str) -> IResult<&'a str, Expression> {
let e2 = expr.clone();
let child = map(preceded(tag("."), raw_ident), move |id| {
Expression::Child(Box::new(expr.clone()), id)
});
let subscript = map(delimited(char('['), integer, char(']')), move |num| {
Expression::Subscript(Box::new(e2.clone()), num)
});
alt((child, subscript))
}
pub fn from_str(input: &str) -> Result<Expression, ErrorKind> {
match ident(input) {
Ok((mut rem, mut expr)) => {
while !rem.is_empty() {
match postfix(expr)(rem) {
Ok((rem_, expr_)) => {
rem = rem_;
expr = expr_;
}
// Forward Incomplete and Error
result => {
return result.map(|(_, o)| o).map_err(to_error_kind);
}
}
}
Ok(expr)
}
// Forward Incomplete and Error
result => result.map(|(_, o)| o).map_err(to_error_kind),
}
}
pub fn to_error_kind(e: Err<nom::error::Error<&str>>) -> ErrorKind {
match e {
Err::Incomplete(_) => ErrorKind::Complete,
Err::Failure(e) | Err::Error(e) => e.code,
}
}
#[cfg(test)]
mod test {
use super::Expression::*;
use super::*;
#[test]
fn test_id() {
let parsed: Expression = from_str("abcd").unwrap();
assert_eq!(parsed, Identifier("abcd".into()));
}
#[test]
fn test_id_dash() {
let parsed: Expression = from_str("abcd-efgh").unwrap();
assert_eq!(parsed, Identifier("abcd-efgh".into()));
}
#[test]
fn test_child() {
let parsed: Expression = from_str("abcd.efgh").unwrap();
let expected = Child(Box::new(Identifier("abcd".into())), "efgh".into());
assert_eq!(parsed, expected);
let parsed: Expression = from_str("abcd.efgh.ijkl").unwrap();
let expected = Child(
Box::new(Child(Box::new(Identifier("abcd".into())), "efgh".into())),
"ijkl".into(),
);
assert_eq!(parsed, expected);
}
#[test]
fn test_subscript() {
let parsed: Expression = from_str("abcd[12]").unwrap();
let expected = Subscript(Box::new(Identifier("abcd".into())), 12);
assert_eq!(parsed, expected);
}
#[test]
fn test_subscript_neg() {
let parsed: Expression = from_str("abcd[-1]").unwrap();
let expected = Subscript(Box::new(Identifier("abcd".into())), -1);
assert_eq!(parsed, expected);
}
}

87
src/config/source.rs Normal file
View file

@ -0,0 +1,87 @@
use crate::config::error::*;
use crate::config::path;
use crate::config::value::{Value, ValueKind};
use std::collections::HashMap;
use std::fmt::Debug;
use std::str::FromStr;
/// Describes a generic _source_ of configuration properties.
pub trait Source: Debug {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync>;
/// Collect all configuration properties available from this source and return a HashMap.
fn collect(&self) -> Result<HashMap<String, Value>>;
fn collect_to(&self, cache: &mut Value) -> Result<()> {
let props = match self.collect() {
Ok(props) => props,
Err(error) => {
return Err(error);
}
};
for (key, val) in &props {
match path::Expression::from_str(key) {
// Set using the path.
Ok(expr) => expr.set(cache, val.clone()),
// Set diretly anyway.
_ => path::Expression::Identifier(key.clone()).set(cache, val.clone()),
}
}
Ok(())
}
}
impl Clone for Box<dyn Source + Send + Sync> {
fn clone(&self) -> Box<dyn Source + Send + Sync> {
self.clone_into_box()
}
}
impl Source for Vec<Box<dyn Source + Send + Sync>> {
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<HashMap<String, Value>> {
let mut cache: Value = HashMap::<String, Value>::new().into();
for source in self {
source.collect_to(&mut cache)?;
}
if let ValueKind::Table(table) = cache.kind {
Ok(table)
} else {
unreachable!();
}
}
}
impl<T> Source for Vec<T>
where
T: Source + Sync + Send,
T: Clone,
T: 'static,
{
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
Box::new((*self).clone())
}
fn collect(&self) -> Result<HashMap<String, Value>> {
let mut cache: Value = HashMap::<String, Value>::new().into();
for source in self {
source.collect_to(&mut cache)?;
}
if let ValueKind::Table(table) = cache.kind {
Ok(table)
} else {
unreachable!();
}
}
}

545
src/config/value.rs Normal file
View file

@ -0,0 +1,545 @@
use crate::config::error::*;
use serde::de::{Deserialize, Deserializer, Visitor};
use std::collections::HashMap;
use std::fmt;
use std::fmt::Display;
/// Underlying kind of the configuration value.
#[derive(Clone, Debug, Default, PartialEq)]
pub enum ValueKind {
#[default]
Nil,
Boolean(bool),
Integer(i64),
Float(f64),
String(String),
Table(Table),
Array(Array),
}
pub type Array = Vec<Value>;
pub type Table = HashMap<String, Value>;
impl<T> From<Option<T>> for ValueKind
where
T: Into<ValueKind>,
{
fn from(value: Option<T>) -> Self {
match value {
Some(value) => value.into(),
None => ValueKind::Nil,
}
}
}
impl From<String> for ValueKind {
fn from(value: String) -> Self {
ValueKind::String(value)
}
}
impl<'a> From<&'a str> for ValueKind {
fn from(value: &'a str) -> Self {
ValueKind::String(value.into())
}
}
impl From<i64> for ValueKind {
fn from(value: i64) -> Self {
ValueKind::Integer(value)
}
}
impl From<f64> for ValueKind {
fn from(value: f64) -> Self {
ValueKind::Float(value)
}
}
impl From<bool> for ValueKind {
fn from(value: bool) -> Self {
ValueKind::Boolean(value)
}
}
impl<T> From<HashMap<String, T>> for ValueKind
where
T: Into<Value>,
{
fn from(values: HashMap<String, T>) -> Self {
let mut r = HashMap::new();
for (k, v) in values {
r.insert(k.clone(), v.into());
}
ValueKind::Table(r)
}
}
impl<T> From<Vec<T>> for ValueKind
where
T: Into<Value>,
{
fn from(values: Vec<T>) -> Self {
let mut l = Vec::new();
for v in values {
l.push(v.into());
}
ValueKind::Array(l)
}
}
impl Display for ValueKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ValueKind::String(ref value) => write!(f, "{}", value),
ValueKind::Boolean(value) => write!(f, "{}", value),
ValueKind::Integer(value) => write!(f, "{}", value),
ValueKind::Float(value) => write!(f, "{}", value),
ValueKind::Nil => write!(f, "nil"),
// TODO: Figure out a nice Display for these
ValueKind::Table(ref table) => write!(f, "{:?}", table),
ValueKind::Array(ref array) => write!(f, "{:?}", array),
}
}
}
/// A configuration value.
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Value {
/// A description of the original location of the value.
///
/// A Value originating from a File might contain:
/// ```text
/// Settings.toml
/// ```
///
/// A Value originating from the environment would contain:
/// ```text
/// the envrionment
/// ```
///
/// A Value originating from a remote source might contain:
/// ```text
/// etcd+http://127.0.0.1:2379
/// ```
origin: Option<String>,
/// Underlying kind of the configuration value.
pub kind: ValueKind,
}
impl Value {
/// Create a new value instance that will remember its source uri.
pub fn new<V>(origin: Option<&String>, kind: V) -> Self
where
V: Into<ValueKind>,
{
Value {
origin: origin.cloned(),
kind: kind.into(),
}
}
/// Attempt to deserialize this value into the requested type.
pub fn try_into<'de, T: Deserialize<'de>>(self) -> Result<T> {
T::deserialize(self)
}
/// Returns `self` as a bool, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_bool(self) -> Result<bool> {
match self.kind {
ValueKind::Boolean(value) => Ok(value),
ValueKind::Integer(value) => Ok(value != 0),
ValueKind::Float(value) => Ok(value != 0.0),
ValueKind::String(ref value) => {
match value.to_lowercase().as_ref() {
"1" | "true" | "on" | "yes" => Ok(true),
"0" | "false" | "off" | "no" => Ok(false),
// Unexpected string value
s => Err(ConfigError::invalid_type(
self.origin.clone(),
Unexpected::Str(s.into()),
"a boolean",
)),
}
}
// Unexpected type
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"a boolean",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"a boolean",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"a boolean",
)),
}
}
/// Returns `self` into an i64, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_int(self) -> Result<i64> {
match self.kind {
ValueKind::Integer(value) => Ok(value),
ValueKind::String(ref s) => {
match s.to_lowercase().as_ref() {
"true" | "on" | "yes" => Ok(1),
"false" | "off" | "no" => Ok(0),
_ => {
s.parse().map_err(|_| {
// Unexpected string
ConfigError::invalid_type(
self.origin.clone(),
Unexpected::Str(s.clone()),
"an integer",
)
})
}
}
}
#[allow(clippy::bool_to_int_with_if)]
ValueKind::Boolean(value) => Ok(if value { 1 } else { 0 }),
ValueKind::Float(value) => Ok(value.round() as i64),
// Unexpected type
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"an integer",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"an integer",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"an integer",
)),
}
}
/// Returns `self` into a f64, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_float(self) -> Result<f64> {
match self.kind {
ValueKind::Float(value) => Ok(value),
ValueKind::String(ref s) => {
match s.to_lowercase().as_ref() {
"true" | "on" | "yes" => Ok(1.0),
"false" | "off" | "no" => Ok(0.0),
_ => {
s.parse().map_err(|_| {
// Unexpected string
ConfigError::invalid_type(
self.origin.clone(),
Unexpected::Str(s.clone()),
"a floating point",
)
})
}
}
}
ValueKind::Integer(value) => Ok(value as f64),
ValueKind::Boolean(value) => Ok(if value { 1.0 } else { 0.0 }),
// Unexpected type.
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"a floating point",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"a floating point",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"a floating point",
)),
}
}
/// Returns `self` into a str, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_str(self) -> Result<String> {
match self.kind {
ValueKind::String(value) => Ok(value),
ValueKind::Boolean(value) => Ok(value.to_string()),
ValueKind::Integer(value) => Ok(value.to_string()),
ValueKind::Float(value) => Ok(value.to_string()),
// Cannot convert.
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"a string",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"a string",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"a string",
)),
}
}
/// Returns `self` into an array, if possible.
// FIXME: Should this not be `try_into_*` ?
pub fn into_array(self) -> Result<Vec<Value>> {
match self.kind {
ValueKind::Array(value) => Ok(value),
// Cannot convert.
ValueKind::Float(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Float(value),
"an array",
)),
ValueKind::String(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Str(value),
"an array",
)),
ValueKind::Integer(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Integer(value),
"an array",
)),
ValueKind::Boolean(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Bool(value),
"an array",
)),
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"an array",
)),
ValueKind::Table(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Map,
"an array",
)),
}
}
/// If the `Value` is a Table, returns the associated Map.
// FIXME: Should this not be `try_into_*` ?
pub fn into_table(self) -> Result<HashMap<String, Value>> {
match self.kind {
ValueKind::Table(value) => Ok(value),
// Cannot convert.
ValueKind::Float(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Float(value),
"a map",
)),
ValueKind::String(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Str(value),
"a map",
)),
ValueKind::Integer(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Integer(value),
"a map",
)),
ValueKind::Boolean(value) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Bool(value),
"a map",
)),
ValueKind::Nil => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Unit,
"a map",
)),
ValueKind::Array(_) => Err(ConfigError::invalid_type(
self.origin,
Unexpected::Seq,
"a map",
)),
}
}
}
impl<'de> Deserialize<'de> for Value {
#[inline]
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Value, D::Error>
where
D: Deserializer<'de>,
{
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Value;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("any valid configuration value")
}
#[inline]
fn visit_bool<E>(self, value: bool) -> ::std::result::Result<Value, E> {
Ok(value.into())
}
#[inline]
fn visit_i8<E>(self, value: i8) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_i16<E>(self, value: i16) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_i32<E>(self, value: i32) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_i64<E>(self, value: i64) -> ::std::result::Result<Value, E> {
Ok(value.into())
}
#[inline]
fn visit_u8<E>(self, value: u8) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_u16<E>(self, value: u16) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_u32<E>(self, value: u32) -> ::std::result::Result<Value, E> {
Ok((value as i64).into())
}
#[inline]
fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Value, E> {
// FIXME: This is bad
Ok((value as i64).into())
}
#[inline]
fn visit_f64<E>(self, value: f64) -> ::std::result::Result<Value, E> {
Ok(value.into())
}
#[inline]
fn visit_str<E>(self, value: &str) -> ::std::result::Result<Value, E>
where
E: ::serde::de::Error,
{
self.visit_string(String::from(value))
}
#[inline]
fn visit_string<E>(self, value: String) -> ::std::result::Result<Value, E> {
Ok(value.into())
}
#[inline]
fn visit_none<E>(self) -> ::std::result::Result<Value, E> {
Ok(Value::new(None, ValueKind::Nil))
}
#[inline]
fn visit_some<D>(self, deserializer: D) -> ::std::result::Result<Value, D::Error>
where
D: Deserializer<'de>,
{
Deserialize::deserialize(deserializer)
}
#[inline]
fn visit_unit<E>(self) -> ::std::result::Result<Value, E> {
Ok(Value::new(None, ValueKind::Nil))
}
#[inline]
fn visit_seq<V>(self, mut visitor: V) -> ::std::result::Result<Value, V::Error>
where
V: ::serde::de::SeqAccess<'de>,
{
let mut vec = Array::new();
while let Some(elem) = visitor.next_element()? {
vec.push(elem);
}
Ok(vec.into())
}
fn visit_map<V>(self, mut visitor: V) -> ::std::result::Result<Value, V::Error>
where
V: ::serde::de::MapAccess<'de>,
{
let mut values = Table::new();
while let Some((key, value)) = visitor.next_entry()? {
values.insert(key, value);
}
Ok(values.into())
}
}
deserializer.deserialize_any(ValueVisitor)
}
}
impl<T> From<T> for Value
where
T: Into<ValueKind>,
{
fn from(value: T) -> Self {
Value {
origin: None,
kind: value.into(),
}
}
}
impl Display for Value {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.kind)
}
}

13
src/core.rs Normal file
View file

@ -0,0 +1,13 @@
//! Key types and functions for creating actions, components, packages, and themes.
// API to define functions that alter the behavior of PageTop core.
pub mod action;
// API to build new components.
pub mod component;
// API to add new features with packages.
pub mod package;
// API to add new layouts with themes.
pub mod theme;

10
src/core/action.rs Normal file
View file

@ -0,0 +1,10 @@
mod definition;
pub use definition::{action_ref, ActionBase, ActionTrait};
mod list;
pub use list::Action;
use list::ActionsList;
mod all;
pub(crate) use all::add_action;
pub use all::{dispatch_actions, KeyAction};

34
src/core/action/all.rs Normal file
View file

@ -0,0 +1,34 @@
use crate::core::action::{Action, ActionsList};
use crate::{Handle, LazyStatic};
use std::collections::HashMap;
use std::sync::RwLock;
pub type KeyAction = (Handle, Option<Handle>, Option<String>);
// Registered actions.
static ACTIONS: LazyStatic<RwLock<HashMap<KeyAction, ActionsList>>> =
LazyStatic::new(|| RwLock::new(HashMap::new()));
pub fn add_action(action: Action) {
let mut actions = ACTIONS.write().unwrap();
let key_action = (
action.handle(),
action.referer_handle(),
action.referer_id(),
);
if let Some(list) = actions.get_mut(&key_action) {
list.add(action);
} else {
actions.insert(key_action, ActionsList::new(action));
}
}
pub fn dispatch_actions<B, F>(key_action: KeyAction, f: F)
where
F: FnMut(&Action) -> B,
{
if let Some(list) = ACTIONS.read().unwrap().get(&key_action) {
list.iter_map(f)
}
}

View file

@ -0,0 +1,31 @@
use crate::{Handle, ImplementHandle, Weight};
use std::any::Any;
pub trait ActionBase: Any {
fn as_ref_any(&self) -> &dyn Any;
}
pub trait ActionTrait: ActionBase + ImplementHandle + Send + Sync {
fn referer_handle(&self) -> Option<Handle> {
None
}
fn referer_id(&self) -> Option<String> {
None
}
fn weight(&self) -> Weight {
0
}
}
impl<C: ActionTrait> ActionBase for C {
fn as_ref_any(&self) -> &dyn Any {
self
}
}
pub fn action_ref<A: 'static>(action: &dyn ActionTrait) -> &A {
action.as_ref_any().downcast_ref::<A>().unwrap()
}

45
src/core/action/list.rs Normal file
View file

@ -0,0 +1,45 @@
use crate::core::action::ActionTrait;
use crate::SmartDefault;
use std::sync::{Arc, RwLock};
pub type Action = Box<dyn ActionTrait>;
#[derive(SmartDefault)]
pub struct ActionsList(Arc<RwLock<Vec<Action>>>);
impl ActionsList {
pub fn new(action: Action) -> Self {
let mut list = ActionsList::default();
list.add(action);
list
}
pub fn add(&mut self, action: Action) {
let mut list = self.0.write().unwrap();
list.push(action);
list.sort_by_key(|a| a.weight());
}
pub fn iter_map<B, F>(&self, f: F)
where
Self: Sized,
F: FnMut(&Action) -> B,
{
let _: Vec<_> = self.0.read().unwrap().iter().map(f).collect();
}
}
#[macro_export]
macro_rules! actions {
() => {
Vec::<Action>::new()
};
( $($action:expr),+ $(,)? ) => {{
let mut v = Vec::<Action>::new();
$(
v.push(Box::new($action));
)*
v
}};
}

20
src/core/component.rs Normal file
View file

@ -0,0 +1,20 @@
mod context;
pub use context::{Context, ContextOp};
pub type FnContextualPath = fn(cx: &Context) -> &str;
mod renderable;
pub use renderable::{FnIsRenderable, Renderable};
mod definition;
pub use definition::{component_as_mut, component_as_ref, ComponentBase, ComponentTrait};
mod classes;
pub use classes::{ImplementClasses, ImplementClassesOp};
mod arc_any;
pub use arc_any::AnyComponents;
pub use arc_any::{ArcAnyComponent, ArcAnyOp};
mod arc_typed;
pub use arc_typed::TypedComponents;
pub use arc_typed::{ArcTypedComponent, ArcTypedOp};

View file

@ -0,0 +1,144 @@
use crate::core::component::{ComponentTrait, Context};
use crate::html::{html, Markup};
use crate::{fn_with, Handle, Weight};
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
#[derive(Clone)]
pub struct ArcAnyComponent(Arc<RwLock<dyn ComponentTrait>>);
impl ArcAnyComponent {
pub fn new(component: impl ComponentTrait) -> Self {
ArcAnyComponent(Arc::new(RwLock::new(component)))
}
// ArcAnyComponent BUILDER.
pub fn set(&mut self, component: impl ComponentTrait) {
self.0 = Arc::new(RwLock::new(component));
}
// ArcAnyComponent GETTERS.
pub fn get(&self) -> RwLockReadGuard<'_, dyn ComponentTrait> {
self.0.read().unwrap()
}
pub fn get_mut(&self) -> RwLockWriteGuard<'_, dyn ComponentTrait> {
self.0.write().unwrap()
}
// ArcAnyComponent RENDER.
pub fn render(&self, cx: &mut Context) -> Markup {
self.0.write().unwrap().render(cx)
}
// ArcAnyComponent HELPERS.
fn handle(&self) -> Handle {
self.0.read().unwrap().handle()
}
fn id(&self) -> String {
self.0.read().unwrap().id().unwrap_or_default()
}
fn weight(&self) -> Weight {
self.0.read().unwrap().weight()
}
}
// *************************************************************************************************
pub enum ArcAnyOp {
Add(ArcAnyComponent),
AddAfterId(&'static str, ArcAnyComponent),
AddBeforeId(&'static str, ArcAnyComponent),
Prepend(ArcAnyComponent),
RemoveById(&'static str),
ReplaceById(&'static str, ArcAnyComponent),
Reset,
}
#[derive(Clone, Default)]
pub struct AnyComponents(Vec<ArcAnyComponent>);
impl AnyComponents {
pub fn new(arc: ArcAnyComponent) -> Self {
AnyComponents::default().with_value(ArcAnyOp::Add(arc))
}
pub(crate) fn merge(mixes: &[Option<&AnyComponents>]) -> Self {
let mut opt = AnyComponents::default();
for m in mixes.iter().flatten() {
opt.0.append(&mut m.0.clone());
}
opt
}
// AnyComponents BUILDER.
#[fn_with]
pub fn alter_value(&mut self, op: ArcAnyOp) -> &mut Self {
match op {
ArcAnyOp::Add(arc) => self.0.push(arc),
ArcAnyOp::AddAfterId(id, arc) => match self.0.iter().position(|c| c.id() == id) {
Some(index) => self.0.insert(index + 1, arc),
_ => self.0.push(arc),
},
ArcAnyOp::AddBeforeId(id, arc) => match self.0.iter().position(|c| c.id() == id) {
Some(index) => self.0.insert(index, arc),
_ => self.0.insert(0, arc),
},
ArcAnyOp::Prepend(arc) => self.0.insert(0, arc),
ArcAnyOp::RemoveById(id) => {
if let Some(index) = self.0.iter().position(|c| c.id() == id) {
self.0.remove(index);
}
}
ArcAnyOp::ReplaceById(id, arc) => {
for c in self.0.iter_mut() {
if c.id() == id {
*c = arc;
break;
}
}
}
ArcAnyOp::Reset => self.0.clear(),
}
self
}
// AnyComponents GETTERS.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn get_by_id(&self, id: impl Into<String>) -> Option<&ArcAnyComponent> {
let id = id.into();
self.0.iter().find(|c| c.id() == id)
}
pub fn iter_by_id(&self, id: impl Into<String>) -> impl Iterator<Item = &ArcAnyComponent> {
let id = id.into();
self.0.iter().filter(move |&c| c.id() == id)
}
pub fn iter_by_handle(&self, handle: Handle) -> impl Iterator<Item = &ArcAnyComponent> {
self.0.iter().filter(move |&c| c.handle() == handle)
}
// AnyComponents RENDER.
pub fn render(&self, cx: &mut Context) -> Markup {
let mut components = self.0.clone();
components.sort_by_key(|c| c.weight());
html! {
@for c in components.iter() {
" " (c.render(cx)) " "
}
}
}
}

View file

@ -0,0 +1,141 @@
use crate::core::component::{ComponentTrait, Context};
use crate::html::{html, Markup};
use crate::{fn_with, Handle, Weight};
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
pub struct ArcTypedComponent<C: ComponentTrait>(Arc<RwLock<C>>);
impl<C: ComponentTrait> Clone for ArcTypedComponent<C> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<C: ComponentTrait> ArcTypedComponent<C> {
pub fn new(component: C) -> Self {
ArcTypedComponent(Arc::new(RwLock::new(component)))
}
// ArcTypedComponent BUILDER.
pub fn set(&mut self, component: C) {
self.0 = Arc::new(RwLock::new(component));
}
// ArcTypedComponent GETTERS.
pub fn get(&self) -> RwLockReadGuard<'_, C> {
self.0.read().unwrap()
}
pub fn get_mut(&self) -> RwLockWriteGuard<'_, C> {
self.0.write().unwrap()
}
// ArcTypedComponent RENDER.
pub fn render(&self, cx: &mut Context) -> Markup {
self.0.write().unwrap().render(cx)
}
// ArcTypedComponent HELPERS.
fn handle(&self) -> Handle {
self.0.read().unwrap().handle()
}
fn id(&self) -> String {
self.0.read().unwrap().id().unwrap_or_default()
}
fn weight(&self) -> Weight {
self.0.read().unwrap().weight()
}
}
// *************************************************************************************************
pub enum ArcTypedOp<C: ComponentTrait> {
Add(ArcTypedComponent<C>),
AddAfterId(&'static str, ArcTypedComponent<C>),
AddBeforeId(&'static str, ArcTypedComponent<C>),
Prepend(ArcTypedComponent<C>),
RemoveById(&'static str),
ReplaceById(&'static str, ArcTypedComponent<C>),
Reset,
}
#[derive(Clone, Default)]
pub struct TypedComponents<C: ComponentTrait>(Vec<ArcTypedComponent<C>>);
impl<C: ComponentTrait + Default> TypedComponents<C> {
pub fn new(arc: ArcTypedComponent<C>) -> Self {
TypedComponents::default().with_value(ArcTypedOp::Add(arc))
}
// TypedComponents BUILDER.
#[fn_with]
pub fn alter_value(&mut self, op: ArcTypedOp<C>) -> &mut Self {
match op {
ArcTypedOp::Add(one) => self.0.push(one),
ArcTypedOp::AddAfterId(id, one) => match self.0.iter().position(|c| c.id() == id) {
Some(index) => self.0.insert(index + 1, one),
_ => self.0.push(one),
},
ArcTypedOp::AddBeforeId(id, one) => match self.0.iter().position(|c| c.id() == id) {
Some(index) => self.0.insert(index, one),
_ => self.0.insert(0, one),
},
ArcTypedOp::Prepend(one) => self.0.insert(0, one),
ArcTypedOp::RemoveById(id) => {
if let Some(index) = self.0.iter().position(|c| c.id() == id) {
self.0.remove(index);
}
}
ArcTypedOp::ReplaceById(id, one) => {
for c in self.0.iter_mut() {
if c.id() == id {
*c = one;
break;
}
}
}
ArcTypedOp::Reset => self.0.clear(),
}
self
}
// TypedComponents GETTERS.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn get_by_id(&self, id: impl Into<String>) -> Option<&ArcTypedComponent<C>> {
let id = id.into();
self.0.iter().find(|&c| c.id() == id)
}
pub fn iter_by_id(&self, id: impl Into<String>) -> impl Iterator<Item = &ArcTypedComponent<C>> {
let id = id.into();
self.0.iter().filter(move |&c| c.id() == id)
}
pub fn iter_by_handle(&self, handle: Handle) -> impl Iterator<Item = &ArcTypedComponent<C>> {
self.0.iter().filter(move |&c| c.handle() == handle)
}
// TypedComponents RENDER.
pub fn render(&self, cx: &mut Context) -> Markup {
let mut components = self.0.clone();
components.sort_by_key(|c| c.weight());
html! {
@for c in components.iter() {
" " (c.render(cx)) " "
}
}
}
}

View file

@ -0,0 +1,60 @@
use crate::html::{ClassesOp, OptionClasses};
pub trait ImplementClassesOp {
fn with_classes(self, op: ClassesOp, classes: impl Into<String>) -> Self;
fn add_classes(&mut self, classes: impl Into<String>) -> &mut Self;
fn prepend_classes(&mut self, classes: impl Into<String>) -> &mut Self;
fn remove_classes(&mut self, classes: impl Into<String>) -> &mut Self;
fn replace_classes(&mut self, rep: impl Into<String>, classes: impl Into<String>) -> &mut Self;
fn toggle_classes(&mut self, classes: impl Into<String>) -> &mut Self;
fn set_classes(&mut self, classes: impl Into<String>) -> &mut Self;
}
pub trait ImplementClasses: ImplementClassesOp {
fn alter_classes(&mut self, op: ClassesOp, classes: impl Into<String>) -> &mut Self;
fn classes(&self) -> &OptionClasses;
}
impl<C: ImplementClasses> ImplementClassesOp for C {
fn with_classes(mut self, op: ClassesOp, classes: impl Into<String>) -> Self {
self.alter_classes(op, classes);
self
}
fn add_classes(&mut self, classes: impl Into<String>) -> &mut Self {
self.alter_classes(ClassesOp::Add, classes);
self
}
fn prepend_classes(&mut self, classes: impl Into<String>) -> &mut Self {
self.alter_classes(ClassesOp::Prepend, classes);
self
}
fn remove_classes(&mut self, classes: impl Into<String>) -> &mut Self {
self.alter_classes(ClassesOp::Remove, classes);
self
}
fn replace_classes(&mut self, rep: impl Into<String>, classes: impl Into<String>) -> &mut Self {
self.alter_classes(ClassesOp::Replace(rep.into()), classes);
self
}
fn toggle_classes(&mut self, classes: impl Into<String>) -> &mut Self {
self.alter_classes(ClassesOp::Toggle, classes);
self
}
fn set_classes(&mut self, classes: impl Into<String>) -> &mut Self {
self.alter_classes(ClassesOp::Set, classes);
self
}
}

View file

@ -0,0 +1,153 @@
use crate::base::component::add_base_assets;
use crate::core::theme::all::{theme_by_single_name, THEME};
use crate::core::theme::ThemeRef;
use crate::html::{html, Assets, HeadScript, HeadStyles, JavaScript, Markup, StyleSheet};
use crate::locale::{LanguageIdentifier, LANGID};
use crate::service::HttpRequest;
use crate::{concat_string, util};
use std::collections::HashMap;
use std::str::FromStr;
pub enum ContextOp {
LangId(&'static LanguageIdentifier),
Theme(&'static str),
// Stylesheets.
AddStyleSheet(StyleSheet),
RemoveStyleSheet(&'static str),
// Styles in head.
AddHeadStyles(HeadStyles),
RemoveHeadStyles(&'static str),
// JavaScripts.
AddJavaScript(JavaScript),
RemoveJavaScript(&'static str),
// Scripts in head.
AddHeadScript(HeadScript),
RemoveHeadScript(&'static str),
// Add assets to properly use base components.
AddBaseAssets,
}
#[rustfmt::skip]
pub struct Context {
request : HttpRequest,
langid : &'static LanguageIdentifier,
theme : ThemeRef,
stylesheet: Assets<StyleSheet>, // Stylesheets.
headstyles: Assets<HeadStyles>, // Styles in head.
javascript: Assets<JavaScript>, // JavaScripts.
headscript: Assets<HeadScript>, // Scripts in head.
params : HashMap<&'static str, String>,
id_counter: usize,
}
impl Context {
#[rustfmt::skip]
pub(crate) fn new(request: HttpRequest) -> Self {
Context {
request,
langid : &LANGID,
theme : *THEME,
stylesheet: Assets::<StyleSheet>::new(), // Stylesheets.
headstyles: Assets::<HeadStyles>::new(), // Styles in head.
javascript: Assets::<JavaScript>::new(), // JavaScripts.
headscript: Assets::<HeadScript>::new(), // Scripts in head.
params : HashMap::<&str, String>::new(),
id_counter: 0,
}
}
#[rustfmt::skip]
pub fn alter(&mut self, op: ContextOp) -> &mut Self {
match op {
ContextOp::LangId(langid) => {
self.langid = langid;
}
ContextOp::Theme(theme_name) => {
self.theme = theme_by_single_name(theme_name).unwrap_or(*THEME);
}
// Stylesheets.
ContextOp::AddStyleSheet(css) => { self.stylesheet.add(css); }
ContextOp::RemoveStyleSheet(path) => { self.stylesheet.remove(path); }
// Styles in head.
ContextOp::AddHeadStyles(styles) => { self.headstyles.add(styles); }
ContextOp::RemoveHeadStyles(path) => { self.headstyles.remove(path); }
// JavaScripts.
ContextOp::AddJavaScript(js) => { self.javascript.add(js); }
ContextOp::RemoveJavaScript(path) => { self.javascript.remove(path); }
// Scripts in head.
ContextOp::AddHeadScript(script) => { self.headscript.add(script); }
ContextOp::RemoveHeadScript(path) => { self.headscript.remove(path); }
// Add assets to properly use base components.
ContextOp::AddBaseAssets => { add_base_assets(self); }
}
self
}
pub fn set_param<T: FromStr + ToString>(&mut self, key: &'static str, value: T) -> &mut Self {
self.params.insert(key, value.to_string());
self
}
pub fn remove_param(&mut self, key: &'static str) -> &mut Self {
self.params.remove(key);
self
}
/// Context GETTERS.
pub fn request(&self) -> &HttpRequest {
&self.request
}
pub fn langid(&self) -> &LanguageIdentifier {
self.langid
}
pub fn theme(&self) -> ThemeRef {
self.theme
}
pub fn get_param<T: FromStr + ToString>(&mut self, key: &'static str) -> Option<T> {
if let Some(value) = self.params.get(key) {
if let Ok(value) = T::from_str(value) {
return Some(value);
}
}
None
}
/// Context PREPARE.
pub fn prepare(&mut self) -> Markup {
html! {
(self.stylesheet.prepare()) // Stylesheets.
(self.headstyles.prepare()) // Styles in head.
(self.javascript.prepare()) // JavaScripts.
(self.headscript.prepare()) // Scripts in head.
}
}
// Context EXTRAS.
pub fn required_id<T>(&mut self, id: Option<String>) -> String {
match id {
Some(id) => id,
None => {
let prefix = util::single_type_name::<T>()
.trim()
.replace(' ', "_")
.to_lowercase();
let prefix = if prefix.is_empty() {
"prefix".to_owned()
} else {
prefix
};
self.id_counter += 1;
concat_string!(prefix, "-", self.id_counter.to_string())
}
}
}
}

View file

@ -0,0 +1,106 @@
use crate::base::action;
use crate::core::component::Context;
use crate::html::{html, Markup, PrepareMarkup};
use crate::{util, ImplementHandle, Weight};
use std::any::Any;
pub trait ComponentBase: Any {
fn render(&mut self, cx: &mut Context) -> Markup;
fn as_ref_any(&self) -> &dyn Any;
fn as_mut_any(&mut self) -> &mut dyn Any;
}
pub trait ComponentTrait: ComponentBase + ImplementHandle + Send + Sync {
fn new() -> Self
where
Self: Sized;
fn name(&self) -> String {
util::single_type_name::<Self>().to_owned()
}
fn description(&self) -> Option<String> {
None
}
fn id(&self) -> Option<String> {
None
}
fn weight(&self) -> Weight {
0
}
#[allow(unused_variables)]
fn is_renderable(&self, cx: &Context) -> bool {
true
}
#[allow(unused_variables)]
fn setup_before_prepare(&mut self, cx: &mut Context) {}
#[allow(unused_variables)]
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
PrepareMarkup::None
}
}
impl<C: ComponentTrait> ComponentBase for C {
fn render(&mut self, cx: &mut Context) -> Markup {
if self.is_renderable(cx) {
// Comprueba el componente antes de prepararlo.
self.setup_before_prepare(cx);
// Acciones del tema antes de preparar el componente.
cx.theme().before_prepare_component(self, cx);
// Acciones de los módulos antes de preparar el componente.
action::component::BeforePrepareComponent::dispatch(self, cx, None);
if let Some(id) = self.id() {
action::component::BeforePrepareComponent::dispatch(self, cx, Some(id));
}
// Renderiza el componente.
let markup = match cx.theme().render_component(self, cx) {
Some(html) => html,
None => match self.prepare_component(cx) {
PrepareMarkup::None => html! {},
PrepareMarkup::Text(text) => html! { (text) },
PrepareMarkup::With(html) => html,
},
};
// Acciones del tema después de preparar el componente.
cx.theme().after_prepare_component(self, cx);
// Acciones de los módulos después de preparar el componente.
action::component::AfterPrepareComponent::dispatch(self, cx, None);
if let Some(id) = self.id() {
action::component::AfterPrepareComponent::dispatch(self, cx, Some(id));
}
markup
} else {
html! {}
}
}
fn as_ref_any(&self) -> &dyn Any {
self
}
fn as_mut_any(&mut self) -> &mut dyn Any {
self
}
}
pub fn component_as_ref<C: ComponentTrait>(component: &dyn ComponentTrait) -> Option<&C> {
component.as_ref_any().downcast_ref::<C>()
}
pub fn component_as_mut<C: ComponentTrait>(component: &mut dyn ComponentTrait) -> Option<&mut C> {
component.as_mut_any().downcast_mut::<C>()
}

View file

@ -0,0 +1,14 @@
use crate::core::component::Context;
use crate::SmartDefault;
pub type FnIsRenderable = fn(cx: &Context) -> bool;
#[derive(SmartDefault)]
pub struct Renderable {
#[default(_code = "render_always")]
pub check: FnIsRenderable,
}
fn render_always(_cx: &Context) -> bool {
true
}

4
src/core/package.rs Normal file
View file

@ -0,0 +1,4 @@
mod definition;
pub use definition::{PackageBase, PackageRef, PackageTrait};
pub(crate) mod all;

162
src/core/package/all.rs Normal file
View file

@ -0,0 +1,162 @@
use crate::core::action::add_action;
use crate::core::package::PackageRef;
use crate::core::theme::all::THEMES;
use crate::{config, service, service_for_static_files, static_files, trace, LazyStatic};
#[cfg(feature = "database")]
use crate::db::*;
use std::sync::RwLock;
static_files!(base);
// PACKAGES ****************************************************************************************
static ENABLED_PACKAGES: LazyStatic<RwLock<Vec<PackageRef>>> =
LazyStatic::new(|| RwLock::new(Vec::new()));
static DROPPED_PACKAGES: LazyStatic<RwLock<Vec<PackageRef>>> =
LazyStatic::new(|| RwLock::new(Vec::new()));
// REGISTER PACKAGES *******************************************************************************
pub fn register_packages(app: PackageRef) {
// List of packages to drop.
let mut list: Vec<PackageRef> = Vec::new();
add_to_dropped(&mut list, app);
DROPPED_PACKAGES.write().unwrap().append(&mut list);
// List of packages to enable.
let mut list: Vec<PackageRef> = Vec::new();
// Enable default themes.
add_to_enabled(&mut list, &crate::base::theme::Basic);
add_to_enabled(&mut list, &crate::base::theme::Chassis);
add_to_enabled(&mut list, &crate::base::theme::Inception);
// Enable application packages.
add_to_enabled(&mut list, app);
list.reverse();
ENABLED_PACKAGES.write().unwrap().append(&mut list);
}
fn add_to_dropped(list: &mut Vec<PackageRef>, package: PackageRef) {
for d in package.drop_packages().iter() {
if !list.iter().any(|p| p.handle() == d.handle()) {
list.push(*d);
trace::debug!("Package \"{}\" dropped", d.single_name());
}
}
for d in package.dependencies().iter() {
add_to_dropped(list, *d);
}
}
fn add_to_enabled(list: &mut Vec<PackageRef>, package: PackageRef) {
if !list.iter().any(|p| p.handle() == package.handle()) {
if DROPPED_PACKAGES
.read()
.unwrap()
.iter()
.any(|p| p.handle() == package.handle())
{
panic!(
"Trying to enable \"{}\" package which is dropped",
package.single_name()
);
} else {
list.push(package);
let mut dependencies = package.dependencies();
dependencies.reverse();
for d in dependencies.iter() {
add_to_enabled(list, *d);
}
if let Some(theme) = package.theme() {
let mut registered_themes = THEMES.write().unwrap();
if !registered_themes
.iter()
.any(|t| t.handle() == theme.handle())
{
registered_themes.push(theme);
trace::debug!("Enabling \"{}\" theme", theme.single_name());
}
} else {
trace::debug!("Enabling \"{}\" package", package.single_name());
}
}
}
}
// REGISTER ACTIONS ********************************************************************************
pub fn register_actions() {
for m in ENABLED_PACKAGES.read().unwrap().iter() {
for a in m.actions().into_iter() {
add_action(a);
}
}
}
// INIT PACKAGES ***********************************************************************************
pub fn init_packages() {
trace::info!("Calling application bootstrap");
for m in ENABLED_PACKAGES.read().unwrap().iter() {
m.init();
}
}
// RUN MIGRATIONS **********************************************************************************
#[cfg(feature = "database")]
pub fn run_migrations() {
if let Some(dbconn) = &*DBCONN {
if let Err(e) = run_now({
struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<MigrationItem> {
let mut migrations = vec![];
for m in ENABLED_PACKAGES.read().unwrap().iter() {
migrations.append(&mut m.migrations());
}
migrations
}
}
Migrator::up(SchemaManagerConnection::Connection(dbconn), None)
}) {
trace::error!("Database upgrade failed ({})", e);
};
if let Err(e) = run_now({
struct Migrator;
impl MigratorTrait for Migrator {
fn migrations() -> Vec<MigrationItem> {
let mut migrations = vec![];
for m in DROPPED_PACKAGES.read().unwrap().iter() {
migrations.append(&mut m.migrations());
}
migrations
}
}
Migrator::down(SchemaManagerConnection::Connection(dbconn), None)
}) {
trace::error!("Database downgrade failed ({})", e);
};
}
}
// CONFIGURE SERVICES ******************************************************************************
pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
service_for_static_files!(
scfg,
base => "/base",
[&config::SETTINGS.dev.pagetop_project_dir, "static/base"]
);
for m in ENABLED_PACKAGES.read().unwrap().iter() {
m.configure_service(scfg);
}
}

View file

@ -0,0 +1,57 @@
use crate::core::action::Action;
use crate::core::theme::ThemeRef;
use crate::locale::L10n;
use crate::{actions, service, util, ImplementHandle};
#[cfg(feature = "database")]
use crate::{db::MigrationItem, migrations};
pub type PackageRef = &'static dyn PackageTrait;
pub trait PackageBase {
fn single_name(&self) -> &'static str;
}
/// Los paquetes deben implementar este *trait*.
pub trait PackageTrait: ImplementHandle + PackageBase + Send + Sync {
fn name(&self) -> L10n {
L10n::n(self.single_name())
}
fn description(&self) -> L10n {
L10n::none()
}
fn theme(&self) -> Option<ThemeRef> {
None
}
fn dependencies(&self) -> Vec<PackageRef> {
vec![]
}
fn drop_packages(&self) -> Vec<PackageRef> {
vec![]
}
fn actions(&self) -> Vec<Action> {
actions![]
}
fn init(&self) {}
#[cfg(feature = "database")]
#[allow(unused_variables)]
fn migrations(&self) -> Vec<MigrationItem> {
migrations![]
}
#[allow(unused_variables)]
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
}
impl<M: ?Sized + PackageTrait> PackageBase for M {
fn single_name(&self) -> &'static str {
util::single_type_name::<Self>()
}
}

8
src/core/theme.rs Normal file
View file

@ -0,0 +1,8 @@
mod definition;
pub use definition::{ThemeRef, ThemeTrait};
mod regions;
pub(crate) use regions::ComponentsInRegions;
pub use regions::{add_component_in, Region};
pub(crate) mod all;

32
src/core/theme/all.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::config;
use crate::core::theme::ThemeRef;
use crate::LazyStatic;
use std::sync::RwLock;
// THEMES ******************************************************************************************
pub static THEMES: LazyStatic<RwLock<Vec<ThemeRef>>> = LazyStatic::new(|| RwLock::new(Vec::new()));
// DEFAULT THEME ***********************************************************************************
pub static THEME: LazyStatic<ThemeRef> =
LazyStatic::new(|| match theme_by_single_name(&config::SETTINGS.app.theme) {
Some(theme) => theme,
None => &crate::base::theme::Inception,
});
// THEME BY NAME ***********************************************************************************
pub fn theme_by_single_name(single_name: &str) -> Option<ThemeRef> {
let single_name = single_name.to_lowercase();
match THEMES
.read()
.unwrap()
.iter()
.find(|t| t.single_name().to_lowercase() == single_name)
{
Some(theme) => Some(*theme),
_ => None,
}
}

View file

@ -0,0 +1,174 @@
use crate::core::component::{ComponentTrait, Context};
use crate::core::package::PackageTrait;
use crate::html::{html, Favicon, Markup, OptionId};
use crate::locale::L10n;
use crate::response::page::Page;
use crate::{concat_string, config};
pub type ThemeRef = &'static dyn ThemeTrait;
/// Los temas deben implementar este "trait".
pub trait ThemeTrait: PackageTrait + Send + Sync {
#[rustfmt::skip]
fn regions(&self) -> Vec<(&'static str, L10n)> {
vec![
("header", L10n::l("header")),
("pagetop", L10n::l("pagetop")),
("content", L10n::l("content")),
("sidebar", L10n::l("sidebar")),
("footer", L10n::l("footer")),
]
}
fn prepare_region(&self, page: &mut Page, region: &str) -> Markup {
let render_region = page.components_in(region).render(page.context());
if render_region.is_empty() {
html! {}
} else {
let id = OptionId::new(region).get().unwrap();
let id_inner = concat_string!(id, "__inner");
html! {
div id=(id) class="pt-region" {
div id=(id_inner) class="pt-region__inner" {
(render_region)
}
}
}
}
}
#[allow(unused_variables)]
fn before_prepare_body(&self, page: &mut Page) {}
fn prepare_body(&self, page: &mut Page) -> Markup {
let skip_to = concat_string!("#", page.skip_to().get().unwrap_or("content".to_owned()));
html! {
body class=[page.body_classes().get()] {
@if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
div class="pt-body__skip" {
a href=(skip_to) { (skip) }
}
}
div class="pt-body__wrapper" {
div class="pt-body__regions" {
(self.prepare_region(page, "header"))
(self.prepare_region(page, "pagetop"))
div class="pt-content" {
div class="pt-content__wrapper" {
(self.prepare_region(page, "content"))
(self.prepare_region(page, "sidebar"))
}
}
(self.prepare_region(page, "footer"))
}
}
}
}
}
fn after_prepare_body(&self, page: &mut Page) {
if page.favicon().is_none() {
page.alter_favicon(Some(Favicon::new().with_icon("/base/favicon.ico")));
}
}
fn prepare_head(&self, page: &mut Page) -> Markup {
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
html! {
head {
meta charset="utf-8";
@if let Some(title) = page.title() {
title { (config::SETTINGS.app.name) (" - ") (title) }
} @else {
title { (config::SETTINGS.app.name) }
}
@if let Some(description) = page.description() {
meta name="description" content=(description);
}
meta name="viewport" content=(viewport);
@for (name, content) in page.metadata() {
meta name=(name) content=(content) {}
}
meta http-equiv="X-UA-Compatible" content="IE=edge";
@for (property, content) in page.properties() {
meta property=(property) content=(content) {}
}
@if let Some(favicon) = page.favicon() {
(favicon.prepare())
}
(page.context().prepare())
}
}
}
#[rustfmt::skip]
#[allow(unused_variables)]
fn before_prepare_component(
&self,
component: &mut dyn ComponentTrait,
cx: &mut Context,
) {
/*
Cómo usarlo:
match component.handle() {
BLOCK_COMPONENT => {
let block = component_as_mut::<Block>(component);
block.alter_title("New title");
},
_ => {},
}
*/
}
#[rustfmt::skip]
#[allow(unused_variables)]
fn after_prepare_component(
&self,
component: &mut dyn ComponentTrait,
cx: &mut Context,
) {
/*
Cómo usarlo:
match component.handle() {
BLOCK_COMPONENT => {
let block = component_as_mut::<Block>(component);
block.alter_title("New title");
},
_ => {},
}
*/
}
#[rustfmt::skip]
#[allow(unused_variables)]
fn render_component(
&self,
component: &dyn ComponentTrait,
cx: &mut Context,
) -> Option<Markup> {
None
/*
Cómo usarlo:
match component.handle() {
BLOCK_COMPONENT => {
let block = component_as_ref::<Block>(component);
match block.template() {
"default" => Some(block_default(block)),
_ => None,
}
},
_ => None,
}
*/
}
}

61
src/core/theme/regions.rs Normal file
View file

@ -0,0 +1,61 @@
use crate::core::component::{AnyComponents, ArcAnyComponent, ArcAnyOp};
use crate::core::theme::ThemeRef;
use crate::{Handle, LazyStatic, SmartDefault};
use std::collections::HashMap;
use std::sync::RwLock;
static THEME_REGIONS: LazyStatic<RwLock<HashMap<Handle, ComponentsInRegions>>> =
LazyStatic::new(|| RwLock::new(HashMap::new()));
static COMMON_REGIONS: LazyStatic<RwLock<ComponentsInRegions>> =
LazyStatic::new(|| RwLock::new(ComponentsInRegions::default()));
#[derive(SmartDefault)]
pub struct ComponentsInRegions(HashMap<&'static str, AnyComponents>);
impl ComponentsInRegions {
pub fn new(region: &'static str, arc: ArcAnyComponent) -> Self {
let mut regions = ComponentsInRegions::default();
regions.add_component_in(region, arc);
regions
}
pub fn add_component_in(&mut self, region: &'static str, arc: ArcAnyComponent) {
if let Some(region) = self.0.get_mut(region) {
region.alter_value(ArcAnyOp::Add(arc));
} else {
self.0.insert(region, AnyComponents::new(arc));
}
}
pub fn get_components(&self, theme: ThemeRef, region: &str) -> AnyComponents {
let common = COMMON_REGIONS.read().unwrap();
if let Some(r) = THEME_REGIONS.read().unwrap().get(&theme.handle()) {
AnyComponents::merge(&[common.0.get(region), self.0.get(region), r.0.get(region)])
} else {
AnyComponents::merge(&[common.0.get(region), self.0.get(region)])
}
}
}
pub enum Region {
Named(&'static str),
OfTheme(ThemeRef, &'static str),
}
pub fn add_component_in(region: Region, arc: ArcAnyComponent) {
match region {
Region::Named(name) => {
COMMON_REGIONS.write().unwrap().add_component_in(name, arc);
}
Region::OfTheme(theme, region) => {
let mut regions = THEME_REGIONS.write().unwrap();
if let Some(r) = regions.get_mut(&theme.handle()) {
r.add_component_in(region, arc);
} else {
regions.insert(theme.handle(), ComponentsInRegions::new(region, arc));
}
}
}
}

4
src/datetime.rs Normal file
View file

@ -0,0 +1,4 @@
//! [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date and time handling
//! ([chrono](https://docs.rs/chrono)).
pub use chrono::prelude::*;

178
src/db.rs Normal file
View file

@ -0,0 +1,178 @@
//! Database access.
use crate::{config, trace, LazyStatic};
pub use url::Url as DbUri;
pub use sea_orm::error::{DbErr, RuntimeErr};
pub use sea_orm::{DatabaseConnection as DbConn, ExecResult, QueryResult};
use sea_orm::{ConnectOptions, ConnectionTrait, Database, DatabaseBackend, Statement};
pub(crate) use futures::executor::block_on as run_now;
const DBCONN_NOT_INITIALIZED: &str = "Database connection not initialized";
pub(crate) static DBCONN: LazyStatic<Option<DbConn>> = LazyStatic::new(|| {
if !config::SETTINGS.database.db_name.trim().is_empty() {
trace::info!(
"Connecting to database \"{}\" using a pool of {} connections",
&config::SETTINGS.database.db_name,
&config::SETTINGS.database.max_pool_size
);
let db_uri = match config::SETTINGS.database.db_type.as_str() {
"mysql" | "postgres" => {
let mut tmp_uri = DbUri::parse(
format!(
"{}://{}/{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_host,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap();
tmp_uri
.set_username(config::SETTINGS.database.db_user.as_str())
.unwrap();
// https://github.com/launchbadge/sqlx/issues/1624
tmp_uri
.set_password(Some(config::SETTINGS.database.db_pass.as_str()))
.unwrap();
if config::SETTINGS.database.db_port != 0 {
tmp_uri
.set_port(Some(config::SETTINGS.database.db_port))
.unwrap();
}
tmp_uri
}
"sqlite" => DbUri::parse(
format!(
"{}://{}",
&config::SETTINGS.database.db_type,
&config::SETTINGS.database.db_name
)
.as_str(),
)
.unwrap(),
_ => {
trace::error!(
"Unrecognized database type \"{}\"",
&config::SETTINGS.database.db_type
);
DbUri::parse("").unwrap()
}
};
Some(
run_now(Database::connect::<ConnectOptions>({
let mut db_opt = ConnectOptions::new(db_uri.to_string());
db_opt.max_connections(config::SETTINGS.database.max_pool_size);
db_opt
}))
.unwrap_or_else(|_| panic!("Failed to connect to database")),
)
} else {
None
}
});
pub async fn query<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Vec<QueryResult>, DbErr> {
match &*DBCONN {
Some(dbconn) => {
let dbbackend = dbconn.get_database_backend();
dbconn
.query_all(Statement::from_string(
dbbackend,
match dbbackend {
DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
},
))
.await
}
None => Err(DbErr::Conn(RuntimeErr::Internal(
DBCONN_NOT_INITIALIZED.to_owned(),
))),
}
}
pub async fn exec<Q: QueryStatementWriter>(stmt: &mut Q) -> Result<Option<QueryResult>, DbErr> {
match &*DBCONN {
Some(dbconn) => {
let dbbackend = dbconn.get_database_backend();
dbconn
.query_one(Statement::from_string(
dbbackend,
match dbbackend {
DatabaseBackend::MySql => stmt.to_string(MysqlQueryBuilder),
DatabaseBackend::Postgres => stmt.to_string(PostgresQueryBuilder),
DatabaseBackend::Sqlite => stmt.to_string(SqliteQueryBuilder),
},
))
.await
}
None => Err(DbErr::Conn(RuntimeErr::Internal(
DBCONN_NOT_INITIALIZED.to_owned(),
))),
}
}
pub async fn exec_raw(stmt: String) -> Result<ExecResult, DbErr> {
match &*DBCONN {
Some(dbconn) => {
let dbbackend = dbconn.get_database_backend();
dbconn
.execute(Statement::from_string(dbbackend, stmt))
.await
}
None => Err(DbErr::Conn(RuntimeErr::Internal(
DBCONN_NOT_INITIALIZED.to_owned(),
))),
}
}
// El siguiente módulo migration es una versión simplificada del módulo sea_orm_migration (v0.11.3)
// https://github.com/SeaQL/sea-orm/tree/0.11.3/sea-orm-migration para evitar los errores generados
// por el paradigma modular de PageTop. Se integran los siguientes archivos del original:
//
// lib.rs => db/migration.rs . . . . . . . . . .(descartando algunos módulos y exportaciones)
// connection.rs => db/migration/connection.rs . . . . . . . . . . . . . . . . . . (completo)
// manager.rs => db/migration/manager.rs . . . . . . . . . . . . . . . . . . . . . (completo)
// migrator.rs => db/migration/migrator.rs . . . . . .(suprimiendo la gestión de los errores)
// prelude.rs => db/migration/prelude.rs . . . . . . . . . . . . . . . . . . . (evitando cli)
// seaql_migrations.rs => db/migration/seaql_migrations.rs . . . . . . . . . . . . (completo)
//
mod migration;
pub use migration::prelude::*;
pub type MigrationItem = Box<dyn MigrationTrait>;
#[macro_export]
macro_rules! new_migration {
( $migration:ident ) => {
pub struct $migration;
impl MigrationName for $migration {
fn name(&self) -> &str {
$crate::util::partial_type_name(module_path!(), 1)
}
}
};
}
#[macro_export]
macro_rules! migrations {
() => {
Vec::<MigrationItem>::new()
};
( $($migration_module:ident),+ $(,)? ) => {{
let mut m = Vec::<MigrationItem>::new();
$(
m.push(Box::new(migration::$migration_module::Migration));
)*
m
}};
}

32
src/db/migration.rs Normal file
View file

@ -0,0 +1,32 @@
//pub mod cli;
pub mod connection;
pub mod manager;
pub mod migrator;
pub mod prelude;
pub mod seaql_migrations;
//pub mod util;
pub use connection::*;
pub use manager::*;
//pub use migrator::*;
//pub use async_trait;
//pub use sea_orm;
//pub use sea_orm::sea_query;
use sea_orm::DbErr;
pub trait MigrationName {
fn name(&self) -> &str;
}
/// The migration definition
#[async_trait::async_trait]
pub trait MigrationTrait: MigrationName + Send + Sync {
/// Define actions to perform when applying the migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr>;
/// Define actions to perform when rolling back the migration
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
Err(DbErr::Migration("We Don't Do That Here".to_owned()))
}
}

View file

@ -0,0 +1,148 @@
use futures::Future;
use sea_orm::{
AccessMode, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr,
ExecResult, IsolationLevel, QueryResult, Statement, TransactionError, TransactionTrait,
};
use std::pin::Pin;
pub enum SchemaManagerConnection<'c> {
Connection(&'c DatabaseConnection),
Transaction(&'c DatabaseTransaction),
}
#[async_trait::async_trait]
impl<'c> ConnectionTrait for SchemaManagerConnection<'c> {
fn get_database_backend(&self) -> DbBackend {
match self {
SchemaManagerConnection::Connection(conn) => conn.get_database_backend(),
SchemaManagerConnection::Transaction(trans) => trans.get_database_backend(),
}
}
async fn execute(&self, stmt: Statement) -> Result<ExecResult, DbErr> {
match self {
SchemaManagerConnection::Connection(conn) => conn.execute(stmt).await,
SchemaManagerConnection::Transaction(trans) => trans.execute(stmt).await,
}
}
async fn execute_unprepared(&self, sql: &str) -> Result<ExecResult, DbErr> {
match self {
SchemaManagerConnection::Connection(conn) => conn.execute_unprepared(sql).await,
SchemaManagerConnection::Transaction(trans) => trans.execute_unprepared(sql).await,
}
}
async fn query_one(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
match self {
SchemaManagerConnection::Connection(conn) => conn.query_one(stmt).await,
SchemaManagerConnection::Transaction(trans) => trans.query_one(stmt).await,
}
}
async fn query_all(&self, stmt: Statement) -> Result<Vec<QueryResult>, DbErr> {
match self {
SchemaManagerConnection::Connection(conn) => conn.query_all(stmt).await,
SchemaManagerConnection::Transaction(trans) => trans.query_all(stmt).await,
}
}
fn is_mock_connection(&self) -> bool {
match self {
SchemaManagerConnection::Connection(conn) => conn.is_mock_connection(),
SchemaManagerConnection::Transaction(trans) => trans.is_mock_connection(),
}
}
}
#[async_trait::async_trait]
impl<'c> TransactionTrait for SchemaManagerConnection<'c> {
async fn begin(&self) -> Result<DatabaseTransaction, DbErr> {
match self {
SchemaManagerConnection::Connection(conn) => conn.begin().await,
SchemaManagerConnection::Transaction(trans) => trans.begin().await,
}
}
async fn begin_with_config(
&self,
isolation_level: Option<IsolationLevel>,
access_mode: Option<AccessMode>,
) -> Result<DatabaseTransaction, DbErr> {
match self {
SchemaManagerConnection::Connection(conn) => {
conn.begin_with_config(isolation_level, access_mode).await
}
SchemaManagerConnection::Transaction(trans) => {
trans.begin_with_config(isolation_level, access_mode).await
}
}
}
async fn transaction<F, T, E>(&self, callback: F) -> Result<T, TransactionError<E>>
where
F: for<'a> FnOnce(
&'a DatabaseTransaction,
) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
+ Send,
T: Send,
E: std::error::Error + Send,
{
match self {
SchemaManagerConnection::Connection(conn) => conn.transaction(callback).await,
SchemaManagerConnection::Transaction(trans) => trans.transaction(callback).await,
}
}
async fn transaction_with_config<F, T, E>(
&self,
callback: F,
isolation_level: Option<IsolationLevel>,
access_mode: Option<AccessMode>,
) -> Result<T, TransactionError<E>>
where
F: for<'a> FnOnce(
&'a DatabaseTransaction,
) -> Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>
+ Send,
T: Send,
E: std::error::Error + Send,
{
match self {
SchemaManagerConnection::Connection(conn) => {
conn.transaction_with_config(callback, isolation_level, access_mode)
.await
}
SchemaManagerConnection::Transaction(trans) => {
trans
.transaction_with_config(callback, isolation_level, access_mode)
.await
}
}
}
}
pub trait IntoSchemaManagerConnection<'c>: Send
where
Self: 'c,
{
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c>;
}
impl<'c> IntoSchemaManagerConnection<'c> for SchemaManagerConnection<'c> {
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> {
self
}
}
impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseConnection {
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> {
SchemaManagerConnection::Connection(self)
}
}
impl<'c> IntoSchemaManagerConnection<'c> for &'c DatabaseTransaction {
fn into_schema_manager_connection(self) -> SchemaManagerConnection<'c> {
SchemaManagerConnection::Transaction(self)
}
}

160
src/db/migration/manager.rs Normal file
View file

@ -0,0 +1,160 @@
use super::{IntoSchemaManagerConnection, SchemaManagerConnection};
use sea_orm::sea_query::{
extension::postgres::{TypeAlterStatement, TypeCreateStatement, TypeDropStatement},
ForeignKeyCreateStatement, ForeignKeyDropStatement, IndexCreateStatement, IndexDropStatement,
TableAlterStatement, TableCreateStatement, TableDropStatement, TableRenameStatement,
TableTruncateStatement,
};
use sea_orm::{ConnectionTrait, DbBackend, DbErr, StatementBuilder};
use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite};
/// Helper struct for writing migration scripts in migration file
pub struct SchemaManager<'c> {
conn: SchemaManagerConnection<'c>,
}
impl<'c> SchemaManager<'c> {
pub fn new<T>(conn: T) -> Self
where
T: IntoSchemaManagerConnection<'c>,
{
Self {
conn: conn.into_schema_manager_connection(),
}
}
pub async fn exec_stmt<S>(&self, stmt: S) -> Result<(), DbErr>
where
S: StatementBuilder,
{
let builder = self.conn.get_database_backend();
self.conn.execute(builder.build(&stmt)).await.map(|_| ())
}
pub fn get_database_backend(&self) -> DbBackend {
self.conn.get_database_backend()
}
pub fn get_connection(&self) -> &SchemaManagerConnection<'c> {
&self.conn
}
}
/// Schema Creation
impl<'c> SchemaManager<'c> {
pub async fn create_table(&self, stmt: TableCreateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn create_index(&self, stmt: IndexCreateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn create_foreign_key(&self, stmt: ForeignKeyCreateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn create_type(&self, stmt: TypeCreateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
}
/// Schema Mutation
impl<'c> SchemaManager<'c> {
pub async fn alter_table(&self, stmt: TableAlterStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn drop_table(&self, stmt: TableDropStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn rename_table(&self, stmt: TableRenameStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn truncate_table(&self, stmt: TableTruncateStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn drop_index(&self, stmt: IndexDropStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn drop_foreign_key(&self, stmt: ForeignKeyDropStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn alter_type(&self, stmt: TypeAlterStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
pub async fn drop_type(&self, stmt: TypeDropStatement) -> Result<(), DbErr> {
self.exec_stmt(stmt).await
}
}
/// Schema Inspection
impl<'c> SchemaManager<'c> {
pub async fn has_table<T>(&self, table: T) -> Result<bool, DbErr>
where
T: AsRef<str>,
{
let stmt = match self.conn.get_database_backend() {
DbBackend::MySql => MySql::has_table(table),
DbBackend::Postgres => Postgres::has_table(table),
DbBackend::Sqlite => Sqlite::has_table(table),
};
let builder = self.conn.get_database_backend();
let res = self
.conn
.query_one(builder.build(&stmt))
.await?
.ok_or_else(|| DbErr::Custom("Failed to check table exists".to_owned()))?;
res.try_get("", "has_table")
}
pub async fn has_column<T, C>(&self, table: T, column: C) -> Result<bool, DbErr>
where
T: AsRef<str>,
C: AsRef<str>,
{
let stmt = match self.conn.get_database_backend() {
DbBackend::MySql => MySql::has_column(table, column),
DbBackend::Postgres => Postgres::has_column(table, column),
DbBackend::Sqlite => Sqlite::has_column(table, column),
};
let builder = self.conn.get_database_backend();
let res = self
.conn
.query_one(builder.build(&stmt))
.await?
.ok_or_else(|| DbErr::Custom("Failed to check column exists".to_owned()))?;
res.try_get("", "has_column")
}
pub async fn has_index<T, I>(&self, table: T, index: I) -> Result<bool, DbErr>
where
T: AsRef<str>,
I: AsRef<str>,
{
let stmt = match self.conn.get_database_backend() {
DbBackend::MySql => MySql::has_index(table, index),
DbBackend::Postgres => Postgres::has_index(table, index),
DbBackend::Sqlite => Sqlite::has_index(table, index),
};
let builder = self.conn.get_database_backend();
let res = self
.conn
.query_one(builder.build(&stmt))
.await?
.ok_or_else(|| DbErr::Custom("Failed to check index exists".to_owned()))?;
res.try_get("", "has_index")
}
}

View file

@ -0,0 +1,589 @@
use futures::Future;
use std::collections::HashSet;
use std::fmt::Display;
use std::pin::Pin;
use std::time::SystemTime;
use tracing::info;
use sea_orm::sea_query::{
self, extension::postgres::Type, Alias, Expr, ForeignKey, IntoIden, JoinType, Order, Query,
SelectStatement, SimpleExpr, Table,
};
use sea_orm::{
ActiveModelTrait, ActiveValue, Condition, ConnectionTrait, DbBackend, DbErr, DeriveIden,
DynIden, EntityTrait, FromQueryResult, Iterable, QueryFilter, Schema, Statement,
TransactionTrait,
};
use sea_schema::{mysql::MySql, postgres::Postgres, probe::SchemaProbe, sqlite::Sqlite};
use super::{seaql_migrations, IntoSchemaManagerConnection, MigrationTrait, SchemaManager};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
/// Status of migration
pub enum MigrationStatus {
/// Not yet applied
Pending,
/// Applied
Applied,
}
impl Display for MigrationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let status = match self {
MigrationStatus::Pending => "Pending",
MigrationStatus::Applied => "Applied",
};
write!(f, "{status}")
}
}
pub struct Migration {
migration: Box<dyn MigrationTrait>,
status: MigrationStatus,
}
impl Migration {
/// Get migration name from MigrationName trait implementation
pub fn name(&self) -> &str {
self.migration.name()
}
/// Get migration status
pub fn status(&self) -> MigrationStatus {
self.status
}
}
/// Performing migrations on a database
#[async_trait::async_trait]
pub trait MigratorTrait: Send {
/// Vector of migrations in time sequence
fn migrations() -> Vec<Box<dyn MigrationTrait>>;
/// Name of the migration table, it is `seaql_migrations` by default
fn migration_table_name() -> DynIden {
seaql_migrations::Entity.into_iden()
}
/// Get list of migrations wrapped in `Migration` struct
fn get_migration_files() -> Vec<Migration> {
Self::migrations()
.into_iter()
.map(|migration| Migration {
migration,
status: MigrationStatus::Pending,
})
.collect()
}
/// Get list of applied migrations from database
async fn get_migration_models<C>(db: &C) -> Result<Vec<seaql_migrations::Model>, DbErr>
where
C: ConnectionTrait,
{
Self::install(db).await?;
let stmt = Query::select()
.table_name(Self::migration_table_name())
.columns(seaql_migrations::Column::iter().map(IntoIden::into_iden))
.order_by(seaql_migrations::Column::Version, Order::Asc)
.to_owned();
let builder = db.get_database_backend();
seaql_migrations::Model::find_by_statement(builder.build(&stmt))
.all(db)
.await
}
/// Get list of migrations with status
async fn get_migration_with_status<C>(db: &C) -> Result<Vec<Migration>, DbErr>
where
C: ConnectionTrait,
{
Self::install(db).await?;
let mut migration_files = Self::get_migration_files();
let migration_models = Self::get_migration_models(db).await?;
let migration_in_db: HashSet<String> = migration_models
.into_iter()
.map(|model| model.version)
.collect();
let migration_in_fs: HashSet<String> = migration_files
.iter()
.map(|file| file.migration.name().to_string())
.collect();
let pending_migrations = &migration_in_fs - &migration_in_db;
for migration_file in migration_files.iter_mut() {
if !pending_migrations.contains(migration_file.migration.name()) {
migration_file.status = MigrationStatus::Applied;
}
}
/*
let missing_migrations_in_fs = &migration_in_db - &migration_in_fs;
let errors: Vec<String> = missing_migrations_in_fs
.iter()
.map(|missing_migration| {
format!("Migration file of version '{missing_migration}' is missing, this migration has been applied but its file is missing")
}).collect();
if !errors.is_empty() {
Err(DbErr::Custom(errors.join("\n")))
} else { */
Ok(migration_files)
/* } */
}
/// Get list of pending migrations
async fn get_pending_migrations<C>(db: &C) -> Result<Vec<Migration>, DbErr>
where
C: ConnectionTrait,
{
Self::install(db).await?;
Ok(Self::get_migration_with_status(db)
.await?
.into_iter()
.filter(|file| file.status == MigrationStatus::Pending)
.collect())
}
/// Get list of applied migrations
async fn get_applied_migrations<C>(db: &C) -> Result<Vec<Migration>, DbErr>
where
C: ConnectionTrait,
{
Self::install(db).await?;
Ok(Self::get_migration_with_status(db)
.await?
.into_iter()
.filter(|file| file.status == MigrationStatus::Applied)
.collect())
}
/// Create migration table `seaql_migrations` in the database
async fn install<C>(db: &C) -> Result<(), DbErr>
where
C: ConnectionTrait,
{
let builder = db.get_database_backend();
let schema = Schema::new(builder);
let mut stmt = schema
.create_table_from_entity(seaql_migrations::Entity)
.table_name(Self::migration_table_name());
stmt.if_not_exists();
db.execute(builder.build(&stmt)).await.map(|_| ())
}
/// Check the status of all migrations
async fn status<C>(db: &C) -> Result<(), DbErr>
where
C: ConnectionTrait,
{
Self::install(db).await?;
info!("Checking migration status");
for Migration { migration, status } in Self::get_migration_with_status(db).await? {
info!("Migration '{}'... {}", migration.name(), status);
}
Ok(())
}
/// Drop all tables from the database, then reapply all migrations
async fn fresh<'c, C>(db: C) -> Result<(), DbErr>
where
C: IntoSchemaManagerConnection<'c>,
{
exec_with_connection::<'_, _, _>(db, move |manager| {
Box::pin(async move { exec_fresh::<Self>(manager).await })
})
.await
}
/// Rollback all applied migrations, then reapply all migrations
async fn refresh<'c, C>(db: C) -> Result<(), DbErr>
where
C: IntoSchemaManagerConnection<'c>,
{
exec_with_connection::<'_, _, _>(db, move |manager| {
Box::pin(async move {
exec_down::<Self>(manager, None).await?;
exec_up::<Self>(manager, None).await
})
})
.await
}
/// Rollback all applied migrations
async fn reset<'c, C>(db: C) -> Result<(), DbErr>
where
C: IntoSchemaManagerConnection<'c>,
{
exec_with_connection::<'_, _, _>(db, move |manager| {
Box::pin(async move { exec_down::<Self>(manager, None).await })
})
.await
}
/// Apply pending migrations
async fn up<'c, C>(db: C, steps: Option<u32>) -> Result<(), DbErr>
where
C: IntoSchemaManagerConnection<'c>,
{
exec_with_connection::<'_, _, _>(db, move |manager| {
Box::pin(async move { exec_up::<Self>(manager, steps).await })
})
.await
}
/// Rollback applied migrations
async fn down<'c, C>(db: C, steps: Option<u32>) -> Result<(), DbErr>
where
C: IntoSchemaManagerConnection<'c>,
{
exec_with_connection::<'_, _, _>(db, move |manager| {
Box::pin(async move { exec_down::<Self>(manager, steps).await })
})
.await
}
}
async fn exec_with_connection<'c, C, F>(db: C, f: F) -> Result<(), DbErr>
where
C: IntoSchemaManagerConnection<'c>,
F: for<'b> Fn(
&'b SchemaManager<'_>,
) -> Pin<Box<dyn Future<Output = Result<(), DbErr>> + Send + 'b>>,
{
let db = db.into_schema_manager_connection();
match db.get_database_backend() {
DbBackend::Postgres => {
let transaction = db.begin().await?;
let manager = SchemaManager::new(&transaction);
f(&manager).await?;
transaction.commit().await
}
DbBackend::MySql | DbBackend::Sqlite => {
let manager = SchemaManager::new(db);
f(&manager).await
}
}
}
async fn exec_fresh<M>(manager: &SchemaManager<'_>) -> Result<(), DbErr>
where
M: MigratorTrait + ?Sized,
{
let db = manager.get_connection();
M::install(db).await?;
let db_backend = db.get_database_backend();
// Temporarily disable the foreign key check
if db_backend == DbBackend::Sqlite {
info!("Disabling foreign key check");
db.execute(Statement::from_string(
db_backend,
"PRAGMA foreign_keys = OFF".to_owned(),
))
.await?;
info!("Foreign key check disabled");
}
// Drop all foreign keys
if db_backend == DbBackend::MySql {
info!("Dropping all foreign keys");
let stmt = query_mysql_foreign_keys(db);
let rows = db.query_all(db_backend.build(&stmt)).await?;
for row in rows.into_iter() {
let constraint_name: String = row.try_get("", "CONSTRAINT_NAME")?;
let table_name: String = row.try_get("", "TABLE_NAME")?;
info!(
"Dropping foreign key '{}' from table '{}'",
constraint_name, table_name
);
let mut stmt = ForeignKey::drop();
stmt.table(Alias::new(table_name.as_str()))
.name(constraint_name.as_str());
db.execute(db_backend.build(&stmt)).await?;
info!("Foreign key '{}' has been dropped", constraint_name);
}
info!("All foreign keys dropped");
}
// Drop all tables
let stmt = query_tables(db);
let rows = db.query_all(db_backend.build(&stmt)).await?;
for row in rows.into_iter() {
let table_name: String = row.try_get("", "table_name")?;
info!("Dropping table '{}'", table_name);
let mut stmt = Table::drop();
stmt.table(Alias::new(table_name.as_str()))
.if_exists()
.cascade();
db.execute(db_backend.build(&stmt)).await?;
info!("Table '{}' has been dropped", table_name);
}
// Drop all types
if db_backend == DbBackend::Postgres {
info!("Dropping all types");
let stmt = query_pg_types(db);
let rows = db.query_all(db_backend.build(&stmt)).await?;
for row in rows {
let type_name: String = row.try_get("", "typname")?;
info!("Dropping type '{}'", type_name);
let mut stmt = Type::drop();
stmt.name(Alias::new(&type_name));
db.execute(db_backend.build(&stmt)).await?;
info!("Type '{}' has been dropped", type_name);
}
}
// Restore the foreign key check
if db_backend == DbBackend::Sqlite {
info!("Restoring foreign key check");
db.execute(Statement::from_string(
db_backend,
"PRAGMA foreign_keys = ON".to_owned(),
))
.await?;
info!("Foreign key check restored");
}
// Reapply all migrations
exec_up::<M>(manager, None).await
}
async fn exec_up<M>(manager: &SchemaManager<'_>, mut steps: Option<u32>) -> Result<(), DbErr>
where
M: MigratorTrait + ?Sized,
{
let db = manager.get_connection();
M::install(db).await?;
if let Some(steps) = steps {
info!("Applying {} pending migrations", steps);
} else {
info!("Applying all pending migrations");
}
let migrations = M::get_pending_migrations(db).await?.into_iter();
if migrations.len() == 0 {
info!("No pending migrations");
}
for Migration { migration, .. } in migrations {
if let Some(steps) = steps.as_mut() {
if steps == &0 {
break;
}
*steps -= 1;
}
info!("Applying migration '{}'", migration.name());
migration.up(manager).await?;
info!("Migration '{}' has been applied", migration.name());
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("SystemTime before UNIX EPOCH!");
seaql_migrations::Entity::insert(seaql_migrations::ActiveModel {
version: ActiveValue::Set(migration.name().to_owned()),
applied_at: ActiveValue::Set(now.as_secs() as i64),
})
.table_name(M::migration_table_name())
.exec(db)
.await?;
}
Ok(())
}
async fn exec_down<M>(manager: &SchemaManager<'_>, mut steps: Option<u32>) -> Result<(), DbErr>
where
M: MigratorTrait + ?Sized,
{
let db = manager.get_connection();
M::install(db).await?;
if let Some(steps) = steps {
info!("Rolling back {} applied migrations", steps);
} else {
info!("Rolling back all applied migrations");
}
let migrations = M::get_applied_migrations(db).await?.into_iter().rev();
if migrations.len() == 0 {
info!("No applied migrations");
}
for Migration { migration, .. } in migrations {
if let Some(steps) = steps.as_mut() {
if steps == &0 {
break;
}
*steps -= 1;
}
info!("Rolling back migration '{}'", migration.name());
migration.down(manager).await?;
info!("Migration '{}' has been rollbacked", migration.name());
seaql_migrations::Entity::delete_many()
.filter(Expr::col(seaql_migrations::Column::Version).eq(migration.name()))
.table_name(M::migration_table_name())
.exec(db)
.await?;
}
Ok(())
}
fn query_tables<C>(db: &C) -> SelectStatement
where
C: ConnectionTrait,
{
match db.get_database_backend() {
DbBackend::MySql => MySql::query_tables(),
DbBackend::Postgres => Postgres::query_tables(),
DbBackend::Sqlite => Sqlite::query_tables(),
}
}
fn get_current_schema<C>(db: &C) -> SimpleExpr
where
C: ConnectionTrait,
{
match db.get_database_backend() {
DbBackend::MySql => MySql::get_current_schema(),
DbBackend::Postgres => Postgres::get_current_schema(),
DbBackend::Sqlite => unimplemented!(),
}
}
#[derive(DeriveIden)]
enum InformationSchema {
#[sea_orm(iden = "information_schema")]
Schema,
#[sea_orm(iden = "TABLE_NAME")]
TableName,
#[sea_orm(iden = "CONSTRAINT_NAME")]
ConstraintName,
TableConstraints,
TableSchema,
ConstraintType,
}
fn query_mysql_foreign_keys<C>(db: &C) -> SelectStatement
where
C: ConnectionTrait,
{
let mut stmt = Query::select();
stmt.columns([
InformationSchema::TableName,
InformationSchema::ConstraintName,
])
.from((
InformationSchema::Schema,
InformationSchema::TableConstraints,
))
.cond_where(
Condition::all()
.add(Expr::expr(get_current_schema(db)).equals((
InformationSchema::TableConstraints,
InformationSchema::TableSchema,
)))
.add(
Expr::col((
InformationSchema::TableConstraints,
InformationSchema::ConstraintType,
))
.eq("FOREIGN KEY"),
),
);
stmt
}
#[derive(DeriveIden)]
enum PgType {
Table,
Typname,
Typnamespace,
Typelem,
}
#[derive(DeriveIden)]
enum PgNamespace {
Table,
Oid,
Nspname,
}
fn query_pg_types<C>(db: &C) -> SelectStatement
where
C: ConnectionTrait,
{
let mut stmt = Query::select();
stmt.column(PgType::Typname)
.from(PgType::Table)
.join(
JoinType::LeftJoin,
PgNamespace::Table,
Expr::col((PgNamespace::Table, PgNamespace::Oid))
.equals((PgType::Table, PgType::Typnamespace)),
)
.cond_where(
Condition::all()
.add(
Expr::expr(get_current_schema(db))
.equals((PgNamespace::Table, PgNamespace::Nspname)),
)
.add(Expr::col((PgType::Table, PgType::Typelem)).eq(0)),
);
stmt
}
trait QueryTable {
type Statement;
fn table_name(self, table_name: DynIden) -> Self::Statement;
}
impl QueryTable for SelectStatement {
type Statement = SelectStatement;
fn table_name(mut self, table_name: DynIden) -> SelectStatement {
self.from(table_name);
self
}
}
impl QueryTable for sea_query::TableCreateStatement {
type Statement = sea_query::TableCreateStatement;
fn table_name(mut self, table_name: DynIden) -> sea_query::TableCreateStatement {
self.table(table_name);
self
}
}
impl<A> QueryTable for sea_orm::Insert<A>
where
A: ActiveModelTrait,
{
type Statement = sea_orm::Insert<A>;
fn table_name(mut self, table_name: DynIden) -> sea_orm::Insert<A> {
sea_orm::QueryTrait::query(&mut self).into_table(table_name);
self
}
}
impl<E> QueryTable for sea_orm::DeleteMany<E>
where
E: EntityTrait,
{
type Statement = sea_orm::DeleteMany<E>;
fn table_name(mut self, table_name: DynIden) -> sea_orm::DeleteMany<E> {
sea_orm::QueryTrait::query(&mut self).from_table(table_name);
self
}
}

View file

@ -0,0 +1,13 @@
//pub use super::cli;
pub use super::connection::IntoSchemaManagerConnection;
pub use super::connection::SchemaManagerConnection;
pub use super::manager::SchemaManager;
pub use super::migrator::MigratorTrait;
pub use super::{MigrationName, MigrationTrait};
pub use async_trait;
pub use sea_orm;
pub use sea_orm::sea_query;
pub use sea_orm::sea_query::*;
pub use sea_orm::DeriveIden;
pub use sea_orm::DeriveMigrationName;

View file

@ -0,0 +1,14 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "seaql_migrations")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub version: String,
pub applied_at: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

40
src/html.rs Normal file
View file

@ -0,0 +1,40 @@
//! HTML in code.
mod maud;
pub use maud::{html, html_private, Markup, PreEscaped, DOCTYPE};
mod assets;
pub use assets::headscript::HeadScript;
pub use assets::headstyles::HeadStyles;
pub use assets::javascript::{JavaScript, ModeJS};
pub use assets::stylesheet::{StyleSheet, TargetMedia};
pub use assets::Assets;
mod favicon;
pub use favicon::Favicon;
mod opt_id;
pub use opt_id::OptionId;
mod opt_name;
pub use opt_name::OptionName;
mod opt_string;
pub use opt_string::OptionString;
mod opt_translated;
pub use opt_translated::OptionTranslated;
mod opt_classes;
pub use opt_classes::{ClassesOp, OptionClasses};
mod opt_component;
pub use opt_component::OptionComponent;
pub mod unit;
pub enum PrepareMarkup {
None,
Text(&'static str),
With(Markup),
}

54
src/html/assets.rs Normal file
View file

@ -0,0 +1,54 @@
pub mod headscript;
pub mod headstyles;
pub mod javascript;
pub mod stylesheet;
use crate::html::{html, Markup};
use crate::{SmartDefault, Weight};
pub trait AssetsTrait {
fn path(&self) -> &str;
fn weight(&self) -> Weight;
fn prepare(&self) -> Markup;
}
#[derive(SmartDefault)]
pub struct Assets<T>(Vec<T>);
impl<T: AssetsTrait> Assets<T> {
pub fn new() -> Self {
Assets::<T>(Vec::<T>::new())
}
pub fn add(&mut self, asset: T) -> &mut Self {
match self.0.iter().position(|x| x.path() == asset.path()) {
Some(index) => {
if self.0[index].weight() > asset.weight() {
self.0.remove(index);
self.0.push(asset);
}
}
_ => self.0.push(asset),
};
self
}
pub fn remove(&mut self, path: &'static str) -> &mut Self {
if let Some(index) = self.0.iter().position(|x| x.path() == path) {
self.0.remove(index);
};
self
}
pub fn prepare(&mut self) -> Markup {
let assets = &mut self.0;
assets.sort_by_key(|a| a.weight());
html! {
@for a in assets {
(a.prepare())
}
}
}
}

View file

@ -0,0 +1,44 @@
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup};
use crate::{SmartDefault, Weight};
#[rustfmt::skip]
#[derive(SmartDefault)]
pub struct HeadScript {
path : String,
code : String,
weight: Weight,
}
impl AssetsTrait for HeadScript {
fn path(&self) -> &str {
self.path.as_str()
}
fn weight(&self) -> Weight {
self.weight
}
fn prepare(&self) -> Markup {
html! { script { (self.code) }; }
}
}
impl HeadScript {
pub fn named(path: impl Into<String>) -> Self {
HeadScript {
path: path.into(),
..Default::default()
}
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = code.into().trim().to_owned();
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
}

View file

@ -0,0 +1,44 @@
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup};
use crate::{SmartDefault, Weight};
#[rustfmt::skip]
#[derive(SmartDefault)]
pub struct HeadStyles {
path : String,
styles: String,
weight: Weight,
}
impl AssetsTrait for HeadStyles {
fn path(&self) -> &str {
self.path.as_str()
}
fn weight(&self) -> Weight {
self.weight
}
fn prepare(&self) -> Markup {
html! { styles { (self.styles) }; }
}
}
impl HeadStyles {
pub fn named(path: impl Into<String>) -> Self {
HeadStyles {
path: path.into(),
..Default::default()
}
}
pub fn with_styles(mut self, styles: impl Into<String>) -> Self {
self.styles = styles.into().trim().to_owned();
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
}

View file

@ -0,0 +1,69 @@
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup};
use crate::{SmartDefault, Weight};
#[derive(Default, Eq, PartialEq)]
pub enum ModeJS {
Async,
#[default]
Defer,
Normal,
}
#[rustfmt::skip]
#[derive(SmartDefault)]
pub struct JavaScript {
path : String,
prefix : &'static str,
version: &'static str,
weight : Weight,
mode : ModeJS,
}
impl AssetsTrait for JavaScript {
fn path(&self) -> &str {
self.path.as_str()
}
fn weight(&self) -> Weight {
self.weight
}
fn prepare(&self) -> Markup {
html! {
script type="text/javascript"
src=(crate::concat_string!(self.path, self.prefix, self.version))
async[self.mode == ModeJS::Async]
defer[self.mode == ModeJS::Defer]
{};
}
}
}
impl JavaScript {
pub fn at(path: impl Into<String>) -> Self {
JavaScript {
path: path.into(),
..Default::default()
}
}
pub fn with_version(mut self, version: &'static str) -> Self {
(self.prefix, self.version) = if version.is_empty() {
("", "")
} else {
("?v=", version)
};
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
pub fn with_mode(mut self, mode: ModeJS) -> Self {
self.mode = mode;
self
}
}

View file

@ -0,0 +1,73 @@
use crate::html::assets::AssetsTrait;
use crate::html::{html, Markup};
use crate::{SmartDefault, Weight};
pub enum TargetMedia {
Default,
Print,
Screen,
Speech,
}
#[rustfmt::skip]
#[derive(SmartDefault)]
pub struct StyleSheet {
path : String,
prefix : &'static str,
version: &'static str,
media : Option<&'static str>,
weight : Weight,
}
impl AssetsTrait for StyleSheet {
fn path(&self) -> &str {
self.path.as_str()
}
fn weight(&self) -> Weight {
self.weight
}
fn prepare(&self) -> Markup {
html! {
link
rel="stylesheet"
href=(crate::concat_string!(self.path, self.prefix, self.version))
media=[self.media];
}
}
}
impl StyleSheet {
pub fn at(path: impl Into<String>) -> Self {
StyleSheet {
path: path.into(),
..Default::default()
}
}
pub fn with_version(mut self, version: &'static str) -> Self {
(self.prefix, self.version) = if version.is_empty() {
("", "")
} else {
("?v=", version)
};
self
}
pub fn with_weight(mut self, value: Weight) -> Self {
self.weight = value;
self
}
#[rustfmt::skip]
pub fn for_media(mut self, media: TargetMedia) -> Self {
self.media = match media {
TargetMedia::Print => Some("print"),
TargetMedia::Screen => Some("screen"),
TargetMedia::Speech => Some("speech"),
_ => None,
};
self
}
}

93
src/html/favicon.rs Normal file
View file

@ -0,0 +1,93 @@
use crate::html::{html, Markup};
use crate::SmartDefault;
#[derive(SmartDefault)]
pub struct Favicon(Vec<Markup>);
impl Favicon {
pub fn new() -> Self {
Favicon::default()
}
// Favicon BUILDER.
pub fn with_icon(self, image: &str) -> Self {
self.add_icon_item("icon", image, None, None)
}
pub fn with_icon_for_sizes(self, image: &str, sizes: &str) -> Self {
self.add_icon_item("icon", image, Some(sizes), None)
}
pub fn with_apple_touch_icon(self, image: &str, sizes: &str) -> Self {
self.add_icon_item("apple-touch-icon", image, Some(sizes), None)
}
pub fn with_mask_icon(self, image: &str, color: &str) -> Self {
self.add_icon_item("mask-icon", image, None, Some(color))
}
pub fn with_manifest(self, file: &str) -> Self {
self.add_icon_item("manifest", file, None, None)
}
pub fn with_theme_color(mut self, color: &str) -> Self {
self.0.push(html! {
meta name="theme-color" content=(color);
});
self
}
pub fn with_ms_tile_color(mut self, color: &str) -> Self {
self.0.push(html! {
meta name="msapplication-TileColor" content=(color);
});
self
}
pub fn with_ms_tile_image(mut self, image: &str) -> Self {
self.0.push(html! {
meta name="msapplication-TileImage" content=(image);
});
self
}
fn add_icon_item(
mut self,
icon_rel: &str,
icon_source: &str,
icon_sizes: Option<&str>,
icon_color: Option<&str>,
) -> Self {
let icon_type = match icon_source.rfind('.') {
Some(i) => match icon_source[i..].to_owned().to_lowercase().as_str() {
".gif" => Some("image/gif"),
".ico" => Some("image/x-icon"),
".jpg" => Some("image/jpg"),
".png" => Some("image/png"),
".svg" => Some("image/svg+xml"),
_ => None,
},
_ => None,
};
self.0.push(html! {
link
rel=(icon_rel)
type=[(icon_type)]
sizes=[(icon_sizes)]
color=[(icon_color)]
href=(icon_source);
});
self
}
// Favicon PREPARE.
pub(crate) fn prepare(&self) -> Markup {
html! {
@for item in &self.0 {
(item)
}
}
}
}

350
src/html/maud.rs Normal file
View file

@ -0,0 +1,350 @@
//#![no_std]
//! A macro for writing HTML templates.
//!
//! This documentation only describes the runtime API. For a general
//! guide, check out the [book] instead.
//!
//! [book]: https://maud.lambda.xyz/
//#![doc(html_root_url = "https://docs.rs/maud/0.25.0")]
extern crate alloc;
use alloc::{borrow::Cow, boxed::Box, string::String};
use core::fmt::{self, Arguments, Display, Write};
pub use pagetop_macros::html;
mod escape;
/// An adapter that escapes HTML special characters.
///
/// The following characters are escaped:
///
/// * `&` is escaped as `&amp;`
/// * `<` is escaped as `&lt;`
/// * `>` is escaped as `&gt;`
/// * `"` is escaped as `&quot;`
///
/// All other characters are passed through unchanged.
///
/// **Note:** In versions prior to 0.13, the single quote (`'`) was
/// escaped as well.
///
/// # Example
///
/// ```rust
/// use maud::Escaper;
/// use std::fmt::Write;
/// let mut s = String::new();
/// write!(Escaper::new(&mut s), "<script>launchMissiles()</script>").unwrap();
/// assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
/// ```
pub struct Escaper<'a>(&'a mut String);
impl<'a> Escaper<'a> {
/// Creates an `Escaper` from a `String`.
pub fn new(buffer: &'a mut String) -> Escaper<'a> {
Escaper(buffer)
}
}
impl<'a> fmt::Write for Escaper<'a> {
fn write_str(&mut self, s: &str) -> fmt::Result {
escape::escape_to_string(s, self.0);
Ok(())
}
}
/// Represents a type that can be rendered as HTML.
///
/// To implement this for your own type, override either the `.render()`
/// or `.render_to()` methods; since each is defined in terms of the
/// other, you only need to implement one of them. See the example below.
///
/// # Minimal implementation
///
/// An implementation of this trait must override at least one of
/// `.render()` or `.render_to()`. Since the default definitions of
/// these methods call each other, not doing this will result in
/// infinite recursion.
///
/// # Example
///
/// ```rust
/// use maud::{html, Markup, Render};
///
/// /// Provides a shorthand for linking to a CSS stylesheet.
/// pub struct Stylesheet(&'static str);
///
/// impl Render for Stylesheet {
/// fn render(&self) -> Markup {
/// html! {
/// link rel="stylesheet" type="text/css" href=(self.0);
/// }
/// }
/// }
/// ```
pub trait Render {
/// Renders `self` as a block of `Markup`.
fn render(&self) -> Markup {
let mut buffer = String::new();
self.render_to(&mut buffer);
PreEscaped(buffer)
}
/// Appends a representation of `self` to the given buffer.
///
/// Its default implementation just calls `.render()`, but you may
/// override it with something more efficient.
///
/// Note that no further escaping is performed on data written to
/// the buffer. If you override this method, you must make sure that
/// any data written is properly escaped, whether by hand or using
/// the [`Escaper`](struct.Escaper.html) wrapper struct.
fn render_to(&self, buffer: &mut String) {
buffer.push_str(&self.render().into_string());
}
}
impl Render for str {
fn render_to(&self, w: &mut String) {
escape::escape_to_string(self, w);
}
}
impl Render for String {
fn render_to(&self, w: &mut String) {
str::render_to(self, w);
}
}
impl<'a> Render for Cow<'a, str> {
fn render_to(&self, w: &mut String) {
str::render_to(self, w);
}
}
impl<'a> Render for Arguments<'a> {
fn render_to(&self, w: &mut String) {
let _ = Escaper::new(w).write_fmt(*self);
}
}
impl<'a, T: Render + ?Sized> Render for &'a T {
fn render_to(&self, w: &mut String) {
T::render_to(self, w);
}
}
impl<'a, T: Render + ?Sized> Render for &'a mut T {
fn render_to(&self, w: &mut String) {
T::render_to(self, w);
}
}
impl<T: Render + ?Sized> Render for Box<T> {
fn render_to(&self, w: &mut String) {
T::render_to(self, w);
}
}
macro_rules! impl_render_with_display {
($($ty:ty)*) => {
$(
impl Render for $ty {
fn render_to(&self, w: &mut String) {
// TODO: remove the explicit arg when Rust 1.58 is released
format_args!("{self}", self = self).render_to(w);
}
}
)*
};
}
impl_render_with_display! {
char f32 f64
}
macro_rules! impl_render_with_itoa {
($($ty:ty)*) => {
$(
impl Render for $ty {
fn render_to(&self, w: &mut String) {
w.push_str(itoa::Buffer::new().format(*self));
}
}
)*
};
}
impl_render_with_itoa! {
i8 i16 i32 i64 i128 isize
u8 u16 u32 u64 u128 usize
}
/// Renders a value using its [`Display`] impl.
///
/// # Example
///
/// ```rust
/// use maud::html;
/// use std::net::Ipv4Addr;
///
/// let ip_address = Ipv4Addr::new(127, 0, 0, 1);
///
/// let markup = html! {
/// "My IP address is: "
/// (maud::display(ip_address))
/// };
///
/// assert_eq!(markup.into_string(), "My IP address is: 127.0.0.1");
/// ```
pub fn display(value: impl Display) -> impl Render {
struct DisplayWrapper<T>(T);
impl<T: Display> Render for DisplayWrapper<T> {
fn render_to(&self, w: &mut String) {
format_args!("{0}", self.0).render_to(w);
}
}
DisplayWrapper(value)
}
/// A wrapper that renders the inner value without escaping.
#[derive(Debug, Clone, Copy)]
pub struct PreEscaped<T: AsRef<str>>(pub T);
impl<T: AsRef<str>> Render for PreEscaped<T> {
fn render_to(&self, w: &mut String) {
w.push_str(self.0.as_ref());
}
}
/// A block of markup is a string that does not need to be escaped.
///
/// The `html!` macro expands to an expression of this type.
pub type Markup = PreEscaped<String>;
impl Markup {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl<T: AsRef<str> + Into<String>> PreEscaped<T> {
/// Converts the inner value to a string.
pub fn into_string(self) -> String {
self.0.into()
}
}
impl<T: AsRef<str> + Into<String>> From<PreEscaped<T>> for String {
fn from(value: PreEscaped<T>) -> String {
value.into_string()
}
}
impl<T: AsRef<str> + Default> Default for PreEscaped<T> {
fn default() -> Self {
Self(Default::default())
}
}
/// The literal string `<!DOCTYPE html>`.
///
/// # Example
///
/// A minimal web page:
///
/// ```rust
/// use maud::{DOCTYPE, html};
///
/// let markup = html! {
/// (DOCTYPE)
/// html {
/// head {
/// meta charset="utf-8";
/// title { "Test page" }
/// }
/// body {
/// p { "Hello, world!" }
/// }
/// }
/// };
/// ```
pub const DOCTYPE: PreEscaped<&'static str> = PreEscaped("<!DOCTYPE html>");
mod actix_support {
extern crate alloc;
use crate::html::PreEscaped;
use actix_web::{http::header, HttpRequest, HttpResponse, Responder};
use alloc::string::String;
impl Responder for PreEscaped<String> {
type Body = String;
fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
HttpResponse::Ok()
.content_type(header::ContentType::html())
.message_body(self.0)
.unwrap()
}
}
}
#[doc(hidden)]
pub mod html_private {
extern crate alloc;
use super::{display, Render};
use alloc::string::String;
use core::fmt::Display;
#[doc(hidden)]
#[macro_export]
macro_rules! render_to {
($x:expr, $buffer:expr) => {{
use $crate::html::html_private::*;
match ChooseRenderOrDisplay($x) {
x => (&&x).implements_render_or_display().render_to(x.0, $buffer),
}
}};
}
pub use render_to;
pub struct ChooseRenderOrDisplay<T>(pub T);
pub struct ViaRenderTag;
pub struct ViaDisplayTag;
pub trait ViaRender {
fn implements_render_or_display(&self) -> ViaRenderTag {
ViaRenderTag
}
}
pub trait ViaDisplay {
fn implements_render_or_display(&self) -> ViaDisplayTag {
ViaDisplayTag
}
}
impl<T: Render> ViaRender for &ChooseRenderOrDisplay<T> {}
impl<T: Display> ViaDisplay for ChooseRenderOrDisplay<T> {}
impl ViaRenderTag {
pub fn render_to<T: Render + ?Sized>(self, value: &T, buffer: &mut String) {
value.render_to(buffer);
}
}
impl ViaDisplayTag {
pub fn render_to<T: Display + ?Sized>(self, value: &T, buffer: &mut String) {
display(value).render_to(buffer);
}
}
}

34
src/html/maud/escape.rs Normal file
View file

@ -0,0 +1,34 @@
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!!!! PLEASE KEEP THIS IN SYNC WITH `maud_macros/src/escape.rs` !!!!!
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
extern crate alloc;
use alloc::string::String;
pub fn escape_to_string(input: &str, output: &mut String) {
for b in input.bytes() {
match b {
b'&' => output.push_str("&amp;"),
b'<' => output.push_str("&lt;"),
b'>' => output.push_str("&gt;"),
b'"' => output.push_str("&quot;"),
_ => unsafe { output.as_mut_vec().push(b) },
}
}
}
#[cfg(test)]
mod test {
extern crate alloc;
use super::escape_to_string;
use alloc::string::String;
#[test]
fn it_works() {
let mut s = String::new();
escape_to_string("<script>launchMissiles()</script>", &mut s);
assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
}
}

106
src/html/opt_classes.rs Normal file
View file

@ -0,0 +1,106 @@
//! **OptionClasses** implements a *helper* for dynamically adding class names to components.
//!
//! This *helper* differentiates between default classes (generally associated with styles provided
//! by the theme) and user classes (for customizing components based on application styles).
//!
//! Classes can be added using [Add]. Operations to [Remove], [Replace] or [Toggle] a class, as well
//! as [Clear] all classes, are also provided.
//!
//! **OptionClasses** assumes that the order of the classes is irrelevant
//! (<https://stackoverflow.com/a/1321712>), and duplicate classes will not be allowed.
use crate::{fn_with, SmartDefault};
pub enum ClassesOp {
Add,
Prepend,
Remove,
Replace(String),
Toggle,
Set,
}
#[derive(SmartDefault)]
pub struct OptionClasses(Vec<String>);
impl OptionClasses {
pub fn new(classes: impl Into<String>) -> Self {
OptionClasses::default().with_value(ClassesOp::Prepend, classes)
}
// OptionClasses BUILDER.
#[fn_with]
pub fn alter_value(&mut self, op: ClassesOp, classes: impl Into<String>) -> &mut Self {
let classes: String = classes.into();
let classes: Vec<&str> = classes.split_ascii_whitespace().collect();
match op {
ClassesOp::Add => {
self.add(&classes, self.0.len());
}
ClassesOp::Prepend => {
self.add(&classes, 0);
}
ClassesOp::Remove => {
for class in classes {
self.0.retain(|c| c.ne(&class.to_string()));
}
}
ClassesOp::Replace(classes_to_replace) => {
let mut pos = self.0.len();
let replace: Vec<&str> = classes_to_replace.split_ascii_whitespace().collect();
for class in replace {
if let Some(replace_pos) = self.0.iter().position(|c| c.eq(class)) {
self.0.remove(replace_pos);
if pos > replace_pos {
pos = replace_pos;
}
}
}
self.add(&classes, pos);
}
ClassesOp::Toggle => {
for class in classes {
if !class.is_empty() {
if let Some(pos) = self.0.iter().position(|c| c.eq(class)) {
self.0.remove(pos);
} else {
self.0.push(class.to_string());
}
}
}
}
ClassesOp::Set => {
self.0.clear();
self.add(&classes, 0);
}
}
self
}
#[inline]
fn add(&mut self, classes: &Vec<&str>, mut pos: usize) {
for class in classes {
if !class.is_empty() && !self.0.iter().any(|c| c.eq(class)) {
self.0.insert(pos, class.to_string());
pos += 1;
}
}
}
// OptionClasses GETTERS.
pub fn get(&self) -> Option<String> {
if self.0.is_empty() {
None
} else {
Some(self.0.join(" "))
}
}
pub fn contains(&self, class: impl Into<String>) -> bool {
let class: String = class.into();
self.0.iter().any(|c| c.eq(&class))
}
}

45
src/html/opt_component.rs Normal file
View file

@ -0,0 +1,45 @@
use crate::core::component::{ArcTypedComponent, ComponentTrait, Context};
use crate::fn_with;
use crate::html::{html, Markup};
pub struct OptionComponent<C: ComponentTrait>(Option<ArcTypedComponent<C>>);
impl<C: ComponentTrait> Default for OptionComponent<C> {
fn default() -> Self {
OptionComponent(None)
}
}
impl<C: ComponentTrait> OptionComponent<C> {
pub fn new(component: C) -> Self {
OptionComponent::default().with_value(Some(component))
}
// OptionComponent BUILDER.
#[fn_with]
pub fn alter_value(&mut self, component: Option<C>) -> &mut Self {
if let Some(component) = component {
self.0 = Some(ArcTypedComponent::new(component));
} else {
self.0 = None;
}
self
}
// OptionComponent GETTERS.
pub fn get(&self) -> Option<ArcTypedComponent<C>> {
if let Some(value) = &self.0 {
return Some(value.clone());
}
None
}
pub fn render(&self, cx: &mut Context) -> Markup {
match &self.0 {
Some(component) => component.render(cx),
_ => html! {},
}
}
}

29
src/html/opt_id.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::{fn_with, SmartDefault};
#[derive(SmartDefault)]
pub struct OptionId(Option<String>);
impl OptionId {
pub fn new(value: impl Into<String>) -> Self {
OptionId::default().with_value(value)
}
// OptionId BUILDER.
#[fn_with]
pub fn alter_value(&mut self, value: impl Into<String>) -> &mut Self {
self.0 = Some(value.into().trim().replace(' ', "_"));
self
}
// OptionId GETTERS.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

29
src/html/opt_name.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::{fn_with, SmartDefault};
#[derive(SmartDefault)]
pub struct OptionName(Option<String>);
impl OptionName {
pub fn new(value: impl Into<String>) -> Self {
OptionName::default().with_value(value)
}
// OptionName BUILDER.
#[fn_with]
pub fn alter_value(&mut self, value: impl Into<String>) -> &mut Self {
self.0 = Some(value.into().trim().replace(' ', "_"));
self
}
// OptionName GETTERS.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

29
src/html/opt_string.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::{fn_with, SmartDefault};
#[derive(SmartDefault)]
pub struct OptionString(Option<String>);
impl OptionString {
pub fn new(value: impl Into<String>) -> Self {
OptionString::default().with_value(value)
}
// OptionString BUILDER.
#[fn_with]
pub fn alter_value(&mut self, value: impl Into<String>) -> &mut Self {
self.0 = Some(value.into().trim().to_owned());
self
}
// OptionString GETTERS.
pub fn get(&self) -> Option<String> {
if let Some(value) = &self.0 {
if !value.is_empty() {
return Some(value.to_owned());
}
}
None
}
}

Some files were not shown because too many files have changed in this diff Show more