♻️ Major code restructuring
This commit is contained in:
parent
a96e203bb3
commit
fa66d628a0
221 changed files with 228 additions and 315 deletions
142
src/app.rs
Normal file
142
src/app.rs
Normal 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
28
src/app/figfont.rs
Normal 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
1295
src/app/slant.flf
Normal file
File diff suppressed because it is too large
Load diff
1097
src/app/small.flf
Normal file
1097
src/app/small.flf
Normal file
File diff suppressed because it is too large
Load diff
1301
src/app/speed.flf
Normal file
1301
src/app/speed.flf
Normal file
File diff suppressed because it is too large
Load diff
719
src/app/starwars.flf
Normal file
719
src/app/starwars.flf
Normal 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
7
src/base.rs
Normal 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
3
src/base/action.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod component;
|
||||
|
||||
pub mod page;
|
||||
9
src/base/action/component.rs
Normal file
9
src/base/action/component.rs
Normal 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::*;
|
||||
55
src/base/action/component/after_prepare_component.rs
Normal file
55
src/base/action/component/after_prepare_component.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
src/base/action/component/before_prepare_component.rs
Normal file
55
src/base/action/component/before_prepare_component.rs
Normal 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
9
src/base/action/page.rs
Normal 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::*;
|
||||
34
src/base/action/page/after_prepare_body.rs
Normal file
34
src/base/action/page/after_prepare_body.rs
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
||||
34
src/base/action/page/before_prepare_body.rs
Normal file
34
src/base/action/page/before_prepare_body.rs
Normal 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
202
src/base/component.rs
Normal 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
101
src/base/component/block.rs
Normal 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
|
||||
}
|
||||
}
|
||||
124
src/base/component/branding.rs
Normal file
124
src/base/component/branding.rs
Normal 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
|
||||
}
|
||||
}
|
||||
171
src/base/component/button.rs
Normal file
171
src/base/component/button.rs
Normal 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
|
||||
}
|
||||
}
|
||||
20
src/base/component/error403.rs
Normal file
20
src/base/component/error403.rs
Normal 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") }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
20
src/base/component/error404.rs
Normal file
20
src/base/component/error404.rs
Normal 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
322
src/base/component/flex.rs
Normal 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",
|
||||
})
|
||||
}
|
||||
}
|
||||
152
src/base/component/flex/container.rs
Normal file
152
src/base/component/flex/container.rs
Normal 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
|
||||
}
|
||||
}
|
||||
166
src/base/component/flex/item.rs
Normal file
166
src/base/component/flex/item.rs
Normal 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
|
||||
}
|
||||
}
|
||||
14
src/base/component/form.rs
Normal file
14
src/base/component/form.rs
Normal 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};
|
||||
199
src/base/component/form/action_button.rs
Normal file
199
src/base/component/form/action_button.rs
Normal 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
|
||||
}
|
||||
}
|
||||
200
src/base/component/form/date.rs
Normal file
200
src/base/component/form/date.rs
Normal 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
|
||||
}
|
||||
}
|
||||
130
src/base/component/form/form_main.rs
Normal file
130
src/base/component/form/form_main.rs
Normal 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
|
||||
}
|
||||
}
|
||||
63
src/base/component/form/hidden.rs
Normal file
63
src/base/component/form/hidden.rs
Normal 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
|
||||
}
|
||||
}
|
||||
317
src/base/component/form/input.rs
Normal file
317
src/base/component/form/input.rs
Normal 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
|
||||
}
|
||||
}
|
||||
178
src/base/component/heading.rs
Normal file
178
src/base/component/heading.rs
Normal 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
|
||||
}
|
||||
}
|
||||
35
src/base/component/html.rs
Normal file
35
src/base/component/html.rs
Normal 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
|
||||
}
|
||||
}
|
||||
85
src/base/component/icon.rs
Normal file
85
src/base/component/icon.rs
Normal 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
125
src/base/component/image.rs
Normal 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
|
||||
}
|
||||
}
|
||||
17
src/base/component/menu.rs
Normal file
17
src/base/component/menu.rs
Normal 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};
|
||||
87
src/base/component/menu/element.rs
Normal file
87
src/base/component/menu/element.rs
Normal 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
|
||||
}
|
||||
}
|
||||
79
src/base/component/menu/group.rs
Normal file
79
src/base/component/menu/group.rs
Normal 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
|
||||
}
|
||||
}
|
||||
207
src/base/component/menu/item.rs
Normal file
207
src/base/component/menu/item.rs
Normal 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
|
||||
}
|
||||
}
|
||||
79
src/base/component/menu/megamenu.rs
Normal file
79
src/base/component/menu/megamenu.rs
Normal 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
|
||||
}
|
||||
}
|
||||
107
src/base/component/menu/menu_main.rs
Normal file
107
src/base/component/menu/menu_main.rs
Normal 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
|
||||
}
|
||||
}
|
||||
95
src/base/component/menu/submenu.rs
Normal file
95
src/base/component/menu/submenu.rs
Normal 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
|
||||
}
|
||||
}
|
||||
110
src/base/component/paragraph.rs
Normal file
110
src/base/component/paragraph.rs
Normal 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
|
||||
}
|
||||
}
|
||||
133
src/base/component/powered_by.rs
Normal file
133
src/base/component/powered_by.rs
Normal 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" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/base/component/translate.rs
Normal file
35
src/base/component/translate.rs
Normal 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
|
||||
}
|
||||
}
|
||||
168
src/base/component/wrapper.rs
Normal file
168
src/base/component/wrapper.rs
Normal 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
8
src/base/theme.rs
Normal 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
32
src/base/theme/basic.rs
Normal 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
32
src/base/theme/chassis.rs
Normal 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),
|
||||
));
|
||||
}
|
||||
}
|
||||
32
src/base/theme/inception.rs
Normal file
32
src/base/theme/inception.rs
Normal 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
351
src/config.rs
Normal 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
136
src/config/data.rs
Normal 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
462
src/config/de.rs
Normal 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
222
src/config/error.rs
Normal 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
89
src/config/file.rs
Normal 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
126
src/config/file/source.rs
Normal 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
51
src/config/file/toml.rs
Normal 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
167
src/config/path.rs
Normal 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
131
src/config/path/parser.rs
Normal 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
87
src/config/source.rs
Normal 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
545
src/config/value.rs
Normal 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
13
src/core.rs
Normal 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
10
src/core/action.rs
Normal 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
34
src/core/action/all.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
31
src/core/action/definition.rs
Normal file
31
src/core/action/definition.rs
Normal 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
45
src/core/action/list.rs
Normal 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
20
src/core/component.rs
Normal 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};
|
||||
144
src/core/component/arc_any.rs
Normal file
144
src/core/component/arc_any.rs
Normal 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)) " "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/core/component/arc_typed.rs
Normal file
141
src/core/component/arc_typed.rs
Normal 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)) " "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/core/component/classes.rs
Normal file
60
src/core/component/classes.rs
Normal 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
|
||||
}
|
||||
}
|
||||
153
src/core/component/context.rs
Normal file
153
src/core/component/context.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/core/component/definition.rs
Normal file
106
src/core/component/definition.rs
Normal 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>()
|
||||
}
|
||||
14
src/core/component/renderable.rs
Normal file
14
src/core/component/renderable.rs
Normal 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
4
src/core/package.rs
Normal 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
162
src/core/package/all.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
57
src/core/package/definition.rs
Normal file
57
src/core/package/definition.rs
Normal 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
8
src/core/theme.rs
Normal 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
32
src/core/theme/all.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
174
src/core/theme/definition.rs
Normal file
174
src/core/theme/definition.rs
Normal 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
61
src/core/theme/regions.rs
Normal 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
4
src/datetime.rs
Normal 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
178
src/db.rs
Normal 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
32
src/db/migration.rs
Normal 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()))
|
||||
}
|
||||
}
|
||||
148
src/db/migration/connection.rs
Normal file
148
src/db/migration/connection.rs
Normal 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
160
src/db/migration/manager.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
589
src/db/migration/migrator.rs
Normal file
589
src/db/migration/migrator.rs
Normal 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
|
||||
}
|
||||
}
|
||||
13
src/db/migration/prelude.rs
Normal file
13
src/db/migration/prelude.rs
Normal 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;
|
||||
14
src/db/migration/seaql_migrations.rs
Normal file
14
src/db/migration/seaql_migrations.rs
Normal 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
40
src/html.rs
Normal 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
54
src/html/assets.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/html/assets/headscript.rs
Normal file
44
src/html/assets/headscript.rs
Normal 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
|
||||
}
|
||||
}
|
||||
44
src/html/assets/headstyles.rs
Normal file
44
src/html/assets/headstyles.rs
Normal 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
|
||||
}
|
||||
}
|
||||
69
src/html/assets/javascript.rs
Normal file
69
src/html/assets/javascript.rs
Normal 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
|
||||
}
|
||||
}
|
||||
73
src/html/assets/stylesheet.rs
Normal file
73
src/html/assets/stylesheet.rs
Normal 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
93
src/html/favicon.rs
Normal 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
350
src/html/maud.rs
Normal 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 `&`
|
||||
/// * `<` is escaped as `<`
|
||||
/// * `>` is escaped as `>`
|
||||
/// * `"` is escaped as `"`
|
||||
///
|
||||
/// 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, "<script>launchMissiles()</script>");
|
||||
/// ```
|
||||
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
34
src/html/maud/escape.rs
Normal 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("&"),
|
||||
b'<' => output.push_str("<"),
|
||||
b'>' => output.push_str(">"),
|
||||
b'"' => output.push_str("""),
|
||||
_ => 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, "<script>launchMissiles()</script>");
|
||||
}
|
||||
}
|
||||
106
src/html/opt_classes.rs
Normal file
106
src/html/opt_classes.rs
Normal 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
45
src/html/opt_component.rs
Normal 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
29
src/html/opt_id.rs
Normal 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
29
src/html/opt_name.rs
Normal 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
29
src/html/opt_string.rs
Normal 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
Loading…
Add table
Add a link
Reference in a new issue