🎉 Start refactoring PageTop for Sycamore
This commit is contained in:
parent
9f62955acb
commit
38ca8f1d1c
155 changed files with 2805 additions and 10960 deletions
39
packages/drust/Cargo.toml
Normal file
39
packages/drust/Cargo.toml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
[package]
|
||||
name = "drust"
|
||||
version = "0.0.3"
|
||||
edition = "2021"
|
||||
|
||||
description = "A modern web Content Management System to share your world."
|
||||
|
||||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
pagetop.workspace = true
|
||||
|
||||
|
||||
|
||||
# Packages.
|
||||
#pagetop-admin = { version = "0.0", path = "../pagetop-admin" }
|
||||
#pagetop-user = { version = "0.0", path = "../pagetop-user" }
|
||||
#pagetop-node = { version = "0.0", path = "../pagetop-node" }
|
||||
# Themes.
|
||||
#pagetop-bootsier = { version = "0.0", path = "../pagetop-bootsier" }
|
||||
#pagetop-bulmix = { version = "0.0", path = "../pagetop-bulmix" }
|
||||
|
||||
#[features]
|
||||
#default = [ "mysql" ]
|
||||
#mysql = [
|
||||
# "pagetop-user/mysql",
|
||||
# "pagetop-node/mysql",
|
||||
#]
|
||||
#postgres = [
|
||||
# "pagetop-user/postgres",
|
||||
# "pagetop-node/postgres",
|
||||
#]
|
||||
#sqlite = [
|
||||
# "pagetop-user/sqlite",
|
||||
# "pagetop-node/sqlite",
|
||||
#]
|
||||
6
packages/drust/config/common.toml
Normal file
6
packages/drust/config/common.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[app]
|
||||
name = "Drust"
|
||||
description = "A modern web Content Management System to share your world."
|
||||
|
||||
[database]
|
||||
db_type = "mysql"
|
||||
10
packages/drust/config/default.toml
Normal file
10
packages/drust/config/default.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[app]
|
||||
#theme = "Basic"
|
||||
#theme = "Chassis"
|
||||
theme = "Inception"
|
||||
#theme = "Bootsier"
|
||||
#theme = "Bulmix"
|
||||
language = "es-ES"
|
||||
|
||||
[log]
|
||||
tracing = "Info,pagetop=Debug,sqlx::query=Warn"
|
||||
7
packages/drust/config/local.default.toml
Normal file
7
packages/drust/config/local.default.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[database]
|
||||
db_name = "drust"
|
||||
db_user = "drust"
|
||||
db_pass = "demo"
|
||||
|
||||
[dev]
|
||||
pagetop_project_dir = "/home/manuelcillero/Proyectos/pagetop"
|
||||
22
packages/drust/src/main.rs
Normal file
22
packages/drust/src/main.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
struct Drust;
|
||||
|
||||
impl PackageTrait for Drust {
|
||||
fn dependencies(&self) -> Vec<PackageRef> {
|
||||
vec![
|
||||
// Packages.
|
||||
//&pagetop_admin::Admin,
|
||||
//&pagetop_user::User,
|
||||
//&pagetop_node::Node,
|
||||
// Themes.
|
||||
//&pagetop_bootsier::Bootsier,
|
||||
//&pagetop_bulmix::Bulmix,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&Drust).run()?.await
|
||||
}
|
||||
47
packages/pagetop/Cargo.toml
Normal file
47
packages/pagetop/Cargo.toml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
[package]
|
||||
name = "pagetop"
|
||||
version = "0.0.56"
|
||||
edition = "2021"
|
||||
|
||||
description = "An opinionated web framework to build modular Server-Side Rendering web solutions."
|
||||
|
||||
categories = ["web-programming", "gui", "development-tools", "asynchronous"]
|
||||
keywords = ["pagetop", "web", "framework", "frontend", "ssr"]
|
||||
readme = "../../README.md"
|
||||
|
||||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
authors = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "pagetop"
|
||||
|
||||
[dependencies]
|
||||
concat-string = "1.0.1"
|
||||
figlet-rs = "0.1.5"
|
||||
paste = "1.0.15"
|
||||
terminal_size = "0.4.0"
|
||||
toml = "0.8.19"
|
||||
|
||||
serde.workspace = true
|
||||
static-files.workspace = true
|
||||
|
||||
pagetop-macros.workspace = true
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
actix-web.workspace = true
|
||||
actix-web-files.workspace = true
|
||||
actix-web-static-files.workspace = true
|
||||
actix-session.workspace = true
|
||||
fluent-templates.workspace = true
|
||||
nom.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-appender.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing-actix-web.workspace = true
|
||||
substring.workspace = true
|
||||
unic-langid.workspace = true
|
||||
155
packages/pagetop/src/app.rs
Normal file
155
packages/pagetop/src/app.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
//! Prepare and run an application created with **Pagetop**.
|
||||
|
||||
mod figfont;
|
||||
|
||||
use crate::core::{package, package::PackageRef};
|
||||
use crate::{global, locale, service, trace};
|
||||
|
||||
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
|
||||
use actix_session::storage::CookieSessionStore;
|
||||
use actix_session::SessionMiddleware;
|
||||
|
||||
use substring::Substring;
|
||||
|
||||
use std::io::Error;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
pub struct Application;
|
||||
|
||||
impl Default for Application {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Application {
|
||||
/// Creates a new application instance without any package.
|
||||
pub fn new() -> Self {
|
||||
Self::internal_prepare(None)
|
||||
}
|
||||
|
||||
/// Prepares an application instance with a specific package.
|
||||
pub fn prepare(root_package: PackageRef) -> Self {
|
||||
Self::internal_prepare(Some(root_package))
|
||||
}
|
||||
|
||||
// Internal method to prepare the application, optionally with a package.
|
||||
fn internal_prepare(root_package: Option<PackageRef>) -> Self {
|
||||
// On startup, show the application banner.
|
||||
Self::show_banner();
|
||||
|
||||
// Starts logging and event tracing.
|
||||
LazyLock::force(&trace::TRACING);
|
||||
|
||||
// Validates the default language identifier.
|
||||
LazyLock::force(&locale::LANGID_DEFAULT);
|
||||
|
||||
// Registers the application's packages.
|
||||
package::all::register_packages(root_package);
|
||||
|
||||
// Registers package actions.
|
||||
package::all::register_actions();
|
||||
|
||||
// Initializes the packages.
|
||||
package::all::init_packages();
|
||||
|
||||
Self
|
||||
}
|
||||
|
||||
// Displays the application banner based on the configuration.
|
||||
fn show_banner() {
|
||||
use terminal_size::{terminal_size, Width};
|
||||
|
||||
if global::SETTINGS.app.startup_banner.to_lowercase() != "off" {
|
||||
// Application name, formatted for the terminal width if necessary.
|
||||
let mut app_name = global::SETTINGS.app.name.to_string();
|
||||
if let Some((Width(term_width), _)) = terminal_size() {
|
||||
if term_width >= 80 {
|
||||
let maxlen: usize = ((term_width / 10) - 2).into();
|
||||
let mut app = app_name.substring(0, maxlen).to_owned();
|
||||
if app_name.len() > maxlen {
|
||||
app = format!("{app}...");
|
||||
}
|
||||
if let Some(ff) = figfont::FIGFONT.convert(&app) {
|
||||
app_name = ff.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\n{app_name}");
|
||||
|
||||
// Application description.
|
||||
if !global::SETTINGS.app.description.is_empty() {
|
||||
println!("{}\n", global::SETTINGS.app.description);
|
||||
};
|
||||
|
||||
// PageTop version.
|
||||
println!("Powered by PageTop {}\n", env!("CARGO_PKG_VERSION"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the web server.
|
||||
pub fn run(self) -> Result<service::Server, Error> {
|
||||
// Generate the cookie key.
|
||||
let secret_key = service::cookie::Key::generate();
|
||||
|
||||
// Prepares the web server.
|
||||
Ok(service::HttpServer::new(move || {
|
||||
Self::service_app()
|
||||
.wrap(tracing_actix_web::TracingLogger::default())
|
||||
.wrap(
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
|
||||
.session_lifecycle(match global::SETTINGS.server.session_lifetime {
|
||||
0 => SessionLifecycle::BrowserSession(BrowserSession::default()),
|
||||
_ => SessionLifecycle::PersistentSession(
|
||||
PersistentSession::default().session_ttl(
|
||||
service::cookie::time::Duration::seconds(
|
||||
global::SETTINGS.server.session_lifetime,
|
||||
),
|
||||
),
|
||||
),
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
})
|
||||
.bind(format!(
|
||||
"{}:{}",
|
||||
&global::SETTINGS.server.bind_address,
|
||||
&global::SETTINGS.server.bind_port
|
||||
))?
|
||||
.run())
|
||||
}
|
||||
|
||||
/// Method for testing, returns a service application instance.
|
||||
pub fn test(
|
||||
self,
|
||||
) -> service::App<
|
||||
impl service::Factory<
|
||||
service::Request,
|
||||
Config = (),
|
||||
Response = service::Response<service::BoxBody>,
|
||||
Error = service::Error,
|
||||
InitError = (),
|
||||
>,
|
||||
> {
|
||||
Self::service_app()
|
||||
}
|
||||
|
||||
// Configures the service application.
|
||||
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: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Err(ErrorPage::NotFound(request))
|
||||
}
|
||||
*/
|
||||
30
packages/pagetop/src/app/figfont.rs
Normal file
30
packages/pagetop/src/app/figfont.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use crate::global;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use figlet_rs::FIGfont;
|
||||
|
||||
pub static FIGFONT: LazyLock<FIGfont> = LazyLock::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 global::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.",
|
||||
global::SETTINGS.app.startup_banner,
|
||||
);
|
||||
slant
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
1295
packages/pagetop/src/app/slant.flf
Normal file
1295
packages/pagetop/src/app/slant.flf
Normal file
File diff suppressed because it is too large
Load diff
1097
packages/pagetop/src/app/small.flf
Normal file
1097
packages/pagetop/src/app/small.flf
Normal file
File diff suppressed because it is too large
Load diff
1301
packages/pagetop/src/app/speed.flf
Normal file
1301
packages/pagetop/src/app/speed.flf
Normal file
File diff suppressed because it is too large
Load diff
719
packages/pagetop/src/app/starwars.flf
Normal file
719
packages/pagetop/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 / $@
|
||||
> < $@
|
||||
/ . \ $@
|
||||
/__/ \__\$@
|
||||
$@@
|
||||
____ ____$@
|
||||
\ \ / /$@
|
||||
\ \/ /$ @
|
||||
\_ _/$ @
|
||||
| |$ @
|
||||
|__|$ @
|
||||
$ @@
|
||||
________ $@
|
||||
| / $@
|
||||
`---/ / $@
|
||||
/ / $@
|
||||
/ /----.@
|
||||
/________|@
|
||||
$@@
|
||||
___@
|
||||
/ /@
|
||||
| |$@
|
||||
/ /$ @
|
||||
\ \$ @
|
||||
| |$@
|
||||
\__\@@
|
||||
__ $@
|
||||
| |$@
|
||||
| |$@
|
||||
| |$@
|
||||
| |$@
|
||||
| |$@
|
||||
|__|$@@
|
||||
___ @
|
||||
\ \$ @
|
||||
| | @
|
||||
\ \@
|
||||
/ /@
|
||||
| | @
|
||||
/__/$ @@
|
||||
__ _ @
|
||||
/ \/ |@
|
||||
|_/\__/ @
|
||||
$ @
|
||||
$ @
|
||||
$ @
|
||||
@@
|
||||
_ _ @
|
||||
(_)_(_) @
|
||||
/ \ @
|
||||
/ _ \ @
|
||||
/ ___ \ @
|
||||
/_/ \_\@
|
||||
@@
|
||||
_ _ @
|
||||
(_)_(_)@
|
||||
/ _ \ @
|
||||
| | | |@
|
||||
| |_| |@
|
||||
\___/ @
|
||||
@@
|
||||
_ _ @
|
||||
(_) (_)@
|
||||
| | | |@
|
||||
| | | |@
|
||||
| |_| |@
|
||||
\___/ @
|
||||
@@
|
||||
_ _ @
|
||||
(_) (_)@
|
||||
__ _ @
|
||||
/ _` |@
|
||||
| (_| |@
|
||||
\__,_|@
|
||||
@@
|
||||
_ _ @
|
||||
(_) (_)@
|
||||
___ @
|
||||
/ _ \ @
|
||||
| (_) |@
|
||||
\___/ @
|
||||
@@
|
||||
_ _ @
|
||||
(_) (_)@
|
||||
_ _ @
|
||||
| | | |@
|
||||
| |_| |@
|
||||
\__,_|@
|
||||
@@
|
||||
___ @
|
||||
/ _ \ @
|
||||
| | ) |@
|
||||
| |< < @
|
||||
| | ) |@
|
||||
| ||_/ @
|
||||
|_| @@
|
||||
196
packages/pagetop/src/config.rs
Normal file
196
packages/pagetop/src/config.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
//! 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 [`config_defaults!`](crate::config_defaults) 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,
|
||||
//! }
|
||||
//!
|
||||
//! config_defaults!(SETTINGS: 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::*;
|
||||
//! use crate::config;
|
||||
//!
|
||||
//! fn global_settings() {
|
||||
//! println!("App name: {}", &global::SETTINGS.app.name);
|
||||
//! println!("App description: {}", &global::SETTINGS.app.description);
|
||||
//! println!("Value of PAGETOP_RUN_MODE: {}", &global::SETTINGS.app.run_mode);
|
||||
//! }
|
||||
//!
|
||||
//! fn package_settings() {
|
||||
//! println!("{} - {:?}", &config::SETTINGS.myapp.name, &config::SETTINGS.myapp.description);
|
||||
//! println!("{}", &config::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::join;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
/// Original configuration values in `key = value` pairs gathered from configuration files.
|
||||
pub static CONFIG_DATA: LazyLock<ConfigData> = LazyLock::new(|| {
|
||||
// Identify the configuration directory.
|
||||
let config_dir = env::var("CARGO_MANIFEST_DIR")
|
||||
.map(|manifest_dir| {
|
||||
let manifest_config = Path::new(&manifest_dir).join("config");
|
||||
if manifest_config.exists() {
|
||||
manifest_config.to_string_lossy().to_string()
|
||||
} else {
|
||||
"config".to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| "config".to_string());
|
||||
|
||||
// Execution mode based on the environment variable PAGETOP_RUN_MODE, defaults to 'default'.
|
||||
let run_mode = env::var("PAGETOP_RUN_MODE").unwrap_or_else(|_| "default".into());
|
||||
|
||||
// Initialize settings.
|
||||
let mut settings = ConfigData::default();
|
||||
|
||||
// Merge (optional) configuration files and set the execution mode.
|
||||
settings
|
||||
// First, add the common configuration for all environments. Defaults to 'common.toml'.
|
||||
.merge(File::with_name(&join!(config_dir, "/common.toml")).required(false))
|
||||
.expect("Failed to merge common configuration (common.toml)")
|
||||
// Add the environment-specific configuration. Defaults to 'default.toml'.
|
||||
.merge(File::with_name(&join!(config_dir, "/", run_mode, ".toml")).required(false))
|
||||
.expect(&format!("Failed to merge {run_mode}.toml configuration"))
|
||||
// Add reserved local configuration for the environment. Defaults to 'local.default.toml'.
|
||||
.merge(File::with_name(&join!(config_dir, "/local.", run_mode, ".toml")).required(false))
|
||||
.expect("Failed to merge reserved local environment configuration")
|
||||
// Add the general reserved local configuration. Defaults to 'local.toml'.
|
||||
.merge(File::with_name(&join!(config_dir, "/local.toml")).required(false))
|
||||
.expect("Failed to merge general reserved local configuration")
|
||||
// Save the execution mode.
|
||||
.set("app.run_mode", run_mode)
|
||||
.expect("Failed to set application run mode");
|
||||
|
||||
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! config_defaults {
|
||||
( $SETTINGS:ident: $Settings:ty => [ $($key:literal => $value:literal),* $(,)? ] ) => {
|
||||
#[doc = concat!(
|
||||
"Assigned or predefined values for configuration settings associated to the ",
|
||||
"[`", stringify!($Settings), "`] type."
|
||||
)]
|
||||
pub static $SETTINGS: std::sync::LazyLock<$Settings> = std::sync::LazyLock::new(|| {
|
||||
let mut settings = $crate::config::CONFIG_DATA.clone();
|
||||
$(
|
||||
settings.set_default($key, $value).unwrap();
|
||||
)*
|
||||
match settings.try_into() {
|
||||
Ok(s) => s,
|
||||
Err(e) => panic!("Error parsing settings: {}", e),
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
136
packages/pagetop/src/config/data.rs
Normal file
136
packages/pagetop/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
packages/pagetop/src/config/de.rs
Normal file
462
packages/pagetop/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
packages/pagetop/src/config/error.rs
Normal file
222
packages/pagetop/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())
|
||||
}
|
||||
}
|
||||
85
packages/pagetop/src/config/file.rs
Normal file
85
packages/pagetop/src/config/file.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
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(ConfigError::Foreign) {
|
||||
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
packages/pagetop/src/config/file/source.rs
Normal file
126
packages/pagetop/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 ["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(Component::CurDir)) => comps.push(a),
|
||||
(Some(_), Some(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
packages/pagetop/src/config/file/toml.rs
Normal file
51
packages/pagetop/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
packages/pagetop/src/config/path.rs
Normal file
167
packages/pagetop/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
packages/pagetop/src/config/path/parser.rs
Normal file
131
packages/pagetop/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
packages/pagetop/src/config/source.rs
Normal file
87
packages/pagetop/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
packages/pagetop/src/config/value.rs
Normal file
545
packages/pagetop/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)
|
||||
}
|
||||
}
|
||||
73
packages/pagetop/src/core.rs
Normal file
73
packages/pagetop/src/core.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
//! Key types and functions for creating actions, components, packages, and themes.
|
||||
|
||||
use crate::global::TypeInfo;
|
||||
|
||||
use std::any::Any;
|
||||
|
||||
// Common definitions for core types.
|
||||
pub trait AnyBase: Any {
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
fn short_name(&self) -> &'static str;
|
||||
|
||||
fn as_any_ref(&self) -> &dyn Any;
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
|
||||
#[allow(clippy::inline_always)]
|
||||
impl<T: Any> AnyBase for T {
|
||||
#[inline(always)]
|
||||
fn type_name(&self) -> &'static str {
|
||||
TypeInfo::FullName.of::<T>()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn short_name(&self) -> &'static str {
|
||||
TypeInfo::ShortName.of::<T>()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn as_any_ref(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnyTo: AnyBase {
|
||||
#[inline]
|
||||
fn is<T>(&self) -> bool
|
||||
where
|
||||
T: AnyBase,
|
||||
{
|
||||
self.as_any_ref().is::<T>()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn downcast_ref<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: AnyBase,
|
||||
{
|
||||
self.as_any_ref().downcast_ref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn downcast_mut<T>(&mut self) -> Option<&mut T>
|
||||
where
|
||||
T: AnyBase,
|
||||
{
|
||||
self.as_any_mut().downcast_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized + AnyBase> AnyTo for T {}
|
||||
|
||||
// API to define functions that alter the behavior of PageTop core.
|
||||
pub mod action;
|
||||
|
||||
// API to add new features with packages.
|
||||
pub mod package;
|
||||
19
packages/pagetop/src/core/action.rs
Normal file
19
packages/pagetop/src/core/action.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
mod definition;
|
||||
pub use definition::{ActionBase, ActionBox, ActionKey, ActionTrait};
|
||||
|
||||
mod list;
|
||||
use list::ActionsList;
|
||||
|
||||
mod all;
|
||||
pub(crate) use all::add_action;
|
||||
pub use all::dispatch_actions;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! actions {
|
||||
() => {
|
||||
Vec::<ActionBox>::new()
|
||||
};
|
||||
( $($action:expr),+ $(,)? ) => {{
|
||||
vec![$(Box::new($action),)+]
|
||||
}};
|
||||
}
|
||||
30
packages/pagetop/src/core/action/all.rs
Normal file
30
packages/pagetop/src/core/action/all.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use crate::core::action::{ActionBox, ActionKey, ActionTrait, ActionsList};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
|
||||
// Registered actions.
|
||||
static ACTIONS: LazyLock<RwLock<HashMap<ActionKey, ActionsList>>> =
|
||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
pub fn add_action(action: ActionBox) {
|
||||
let key = action.key();
|
||||
let mut actions = ACTIONS.write().unwrap();
|
||||
if let Some(list) = actions.get_mut(&key) {
|
||||
list.add(action);
|
||||
} else {
|
||||
let mut list = ActionsList::new();
|
||||
list.add(action);
|
||||
actions.insert(key, list);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch_actions<A, B, F>(key: &ActionKey, f: F)
|
||||
where
|
||||
A: ActionTrait,
|
||||
F: FnMut(&A) -> B,
|
||||
{
|
||||
if let Some(list) = ACTIONS.read().unwrap().get(key) {
|
||||
list.iter_map(f);
|
||||
}
|
||||
}
|
||||
61
packages/pagetop/src/core/action/definition.rs
Normal file
61
packages/pagetop/src/core/action/definition.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
use crate::core::AnyBase;
|
||||
use crate::{TypeId, Weight};
|
||||
|
||||
pub type ActionBox = Box<dyn ActionTrait>;
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
pub struct ActionKey {
|
||||
action_type_id: TypeId,
|
||||
theme_type_id: Option<TypeId>,
|
||||
referer_type_id: Option<TypeId>,
|
||||
referer_id: Option<String>,
|
||||
}
|
||||
|
||||
impl ActionKey {
|
||||
pub fn new(
|
||||
action_type_id: TypeId,
|
||||
theme_type_id: Option<TypeId>,
|
||||
referer_type_id: Option<TypeId>,
|
||||
referer_id: Option<String>,
|
||||
) -> Self {
|
||||
ActionKey {
|
||||
action_type_id,
|
||||
theme_type_id,
|
||||
referer_type_id,
|
||||
referer_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ActionBase {
|
||||
fn key(&self) -> ActionKey;
|
||||
}
|
||||
|
||||
pub trait ActionTrait: ActionBase + AnyBase + Send + Sync {
|
||||
fn theme_type_id(&self) -> Option<TypeId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn referer_type_id(&self) -> Option<TypeId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn referer_id(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn weight(&self) -> Weight {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: ActionTrait> ActionBase for A {
|
||||
fn key(&self) -> ActionKey {
|
||||
ActionKey {
|
||||
action_type_id: self.type_id(),
|
||||
theme_type_id: self.theme_type_id(),
|
||||
referer_type_id: self.referer_type_id(),
|
||||
referer_id: self.referer_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
43
packages/pagetop/src/core/action/list.rs
Normal file
43
packages/pagetop/src/core/action/list.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use crate::core::action::{ActionBox, ActionTrait};
|
||||
use crate::core::AnyTo;
|
||||
use crate::trace;
|
||||
use crate::AutoDefault;
|
||||
|
||||
use std::sync::RwLock;
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
pub struct ActionsList(RwLock<Vec<ActionBox>>);
|
||||
|
||||
impl ActionsList {
|
||||
pub fn new() -> Self {
|
||||
ActionsList::default()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, action: ActionBox) {
|
||||
let mut list = self.0.write().unwrap();
|
||||
list.push(action);
|
||||
list.sort_by_key(|a| a.weight());
|
||||
}
|
||||
|
||||
pub fn iter_map<A, B, F>(&self, mut f: F)
|
||||
where
|
||||
Self: Sized,
|
||||
A: ActionTrait,
|
||||
F: FnMut(&A) -> B,
|
||||
{
|
||||
let _: Vec<_> = self
|
||||
.0
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|a| {
|
||||
if let Some(action) = (**a).downcast_ref::<A>() {
|
||||
f(action);
|
||||
} else {
|
||||
trace::error!("Failed to downcast action of type {}", (**a).type_name());
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
4
packages/pagetop/src/core/package.rs
Normal file
4
packages/pagetop/src/core/package.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
mod definition;
|
||||
pub use definition::{PackageRef, PackageTrait};
|
||||
|
||||
pub(crate) mod all;
|
||||
142
packages/pagetop/src/core/package/all.rs
Normal file
142
packages/pagetop/src/core/package/all.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use crate::core::action::add_action;
|
||||
use crate::core::package::PackageRef;
|
||||
use crate::{service, trace};
|
||||
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
|
||||
//static_files!(base);
|
||||
|
||||
// PACKAGES ****************************************************************************************
|
||||
|
||||
static ENABLED_PACKAGES: LazyLock<RwLock<Vec<PackageRef>>> =
|
||||
LazyLock::new(|| RwLock::new(Vec::new()));
|
||||
|
||||
static DROPPED_PACKAGES: LazyLock<RwLock<Vec<PackageRef>>> =
|
||||
LazyLock::new(|| RwLock::new(Vec::new()));
|
||||
|
||||
// REGISTER PACKAGES *******************************************************************************
|
||||
|
||||
pub fn register_packages(root_package: Option<PackageRef>) {
|
||||
// Initialize a list for packages to be enabled.
|
||||
let mut enabled_list: Vec<PackageRef> = Vec::new();
|
||||
|
||||
// Add default welcome page package to the enabled list.
|
||||
// add_to_enabled(&mut enabled_list, &crate::base::package::Welcome);
|
||||
|
||||
// Add default theme packages to the enabled list.
|
||||
// add_to_enabled(&mut enabled_list, &crate::base::theme::Basic);
|
||||
// add_to_enabled(&mut enabled_list, &crate::base::theme::Chassis);
|
||||
// add_to_enabled(&mut enabled_list, &crate::base::theme::Inception);
|
||||
|
||||
// If a root package is provided, add it to the enabled list.
|
||||
if let Some(package) = root_package {
|
||||
add_to_enabled(&mut enabled_list, package);
|
||||
}
|
||||
// Reverse the order to ensure packages are sorted from none to most dependencies.
|
||||
enabled_list.reverse();
|
||||
// Save the final list of enabled packages.
|
||||
ENABLED_PACKAGES.write().unwrap().append(&mut enabled_list);
|
||||
|
||||
// Initialize a list for packages to be dropped.
|
||||
let mut dropped_list: Vec<PackageRef> = Vec::new();
|
||||
// If a root package is provided, analyze its dropped list.
|
||||
if let Some(package) = root_package {
|
||||
add_to_dropped(&mut dropped_list, package);
|
||||
}
|
||||
// Save the final list of dropped packages.
|
||||
DROPPED_PACKAGES.write().unwrap().append(&mut dropped_list);
|
||||
}
|
||||
|
||||
fn add_to_enabled(list: &mut Vec<PackageRef>, package: PackageRef) {
|
||||
// Check if the package is not already in the enabled list to avoid duplicates.
|
||||
if !list.iter().any(|p| p.type_id() == package.type_id()) {
|
||||
// Add the package to the enabled list.
|
||||
list.push(package);
|
||||
|
||||
// Reverse dependencies to add them in correct order (dependencies first).
|
||||
let mut dependencies = package.dependencies();
|
||||
dependencies.reverse();
|
||||
for d in &dependencies {
|
||||
add_to_enabled(list, *d);
|
||||
}
|
||||
|
||||
/* Check if the package has an associated theme to register.
|
||||
if let Some(theme) = package.theme() {
|
||||
let mut registered_themes = THEMES.write().unwrap();
|
||||
// Ensure the theme is not already registered to avoid duplicates.
|
||||
if !registered_themes
|
||||
.iter()
|
||||
.any(|t| t.type_id() == theme.type_id())
|
||||
{
|
||||
registered_themes.push(theme);
|
||||
trace::debug!("Enabling \"{}\" theme", theme.short_name());
|
||||
}
|
||||
} else {
|
||||
trace::debug!("Enabling \"{}\" package", package.short_name());
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
fn add_to_dropped(list: &mut Vec<PackageRef>, package: PackageRef) {
|
||||
// Iterate through packages recommended to be dropped.
|
||||
for d in &package.drop_packages() {
|
||||
// Check if the package is not already in the dropped list.
|
||||
if !list.iter().any(|p| p.type_id() == d.type_id()) {
|
||||
// Check if the package is currently enabled. If so, log a warning.
|
||||
if ENABLED_PACKAGES
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p.type_id() == package.type_id())
|
||||
{
|
||||
trace::warn!(
|
||||
"Trying to drop \"{}\" package which is enabled",
|
||||
package.short_name()
|
||||
);
|
||||
} else {
|
||||
// If the package is not enabled, add it to the dropped list and log the action.
|
||||
list.push(*d);
|
||||
trace::debug!("Package \"{}\" dropped", d.short_name());
|
||||
// Recursively add the dependencies of the dropped package to the dropped list.
|
||||
// This ensures that all dependencies are also considered for dropping.
|
||||
for dependency in &package.dependencies() {
|
||||
add_to_dropped(list, *dependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
// CONFIGURE SERVICES ******************************************************************************
|
||||
|
||||
pub fn configure_services(scfg: &mut service::web::ServiceConfig) {
|
||||
/*
|
||||
static_files_service!(
|
||||
scfg,
|
||||
base => "/base",
|
||||
[&global::SETTINGS.dev.pagetop_project_dir, "static/base"]
|
||||
);
|
||||
*/
|
||||
for m in ENABLED_PACKAGES.read().unwrap().iter() {
|
||||
m.configure_service(scfg);
|
||||
}
|
||||
}
|
||||
34
packages/pagetop/src/core/package/definition.rs
Normal file
34
packages/pagetop/src/core/package/definition.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use crate::core::action::ActionBox;
|
||||
use crate::core::AnyBase;
|
||||
use crate::locale::L10n;
|
||||
use crate::{actions, service};
|
||||
|
||||
pub type PackageRef = &'static dyn PackageTrait;
|
||||
|
||||
/// Los paquetes deben implementar este *trait*.
|
||||
pub trait PackageTrait: AnyBase + Send + Sync {
|
||||
fn name(&self) -> L10n {
|
||||
L10n::n(self.short_name())
|
||||
}
|
||||
|
||||
fn description(&self) -> L10n {
|
||||
L10n::none()
|
||||
}
|
||||
|
||||
fn dependencies(&self) -> Vec<PackageRef> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn drop_packages(&self) -> Vec<PackageRef> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn actions(&self) -> Vec<ActionBox> {
|
||||
actions![]
|
||||
}
|
||||
|
||||
fn init(&self) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {}
|
||||
}
|
||||
288
packages/pagetop/src/global.rs
Normal file
288
packages/pagetop/src/global.rs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
//! Global settings, functions and macro helpers.
|
||||
|
||||
use crate::{config_defaults, trace};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// *************************************************************************************************
|
||||
// SETTINGS.
|
||||
// *************************************************************************************************
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
/// Configuration settings for global [`[app]`](App), [`[dev]`](Dev), [`[log]`](Log), and
|
||||
/// [`[server]`](Server) sections (see [`SETTINGS`]).
|
||||
pub struct Settings {
|
||||
pub app: App,
|
||||
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: *"My App"*.
|
||||
pub name: String,
|
||||
/// Una descripción breve de la aplicación.
|
||||
/// Por defecto: *"Developed with the amazing 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 `[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,
|
||||
}
|
||||
|
||||
config_defaults!(SETTINGS: Settings => [
|
||||
// [app]
|
||||
"app.name" => "My App",
|
||||
"app.description" => "Developed with the amazing PageTop framework.",
|
||||
"app.theme" => "Default",
|
||||
"app.language" => "en-US",
|
||||
"app.direction" => "ltr",
|
||||
"app.startup_banner" => "Slant",
|
||||
|
||||
// [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" => 604_800,
|
||||
]);
|
||||
|
||||
// *************************************************************************************************
|
||||
// FUNCTIONS HELPERS.
|
||||
// *************************************************************************************************
|
||||
|
||||
pub enum TypeInfo {
|
||||
FullName,
|
||||
ShortName,
|
||||
NameFrom(isize),
|
||||
NameTo(isize),
|
||||
PartialName(isize, isize),
|
||||
}
|
||||
|
||||
impl TypeInfo {
|
||||
pub fn of<T: ?Sized>(&self) -> &'static str {
|
||||
let type_name = std::any::type_name::<T>();
|
||||
match self {
|
||||
TypeInfo::FullName => type_name,
|
||||
TypeInfo::ShortName => Self::partial(type_name, -1, None),
|
||||
TypeInfo::NameFrom(start) => Self::partial(type_name, *start, None),
|
||||
TypeInfo::NameTo(end) => Self::partial(type_name, 0, Some(*end)),
|
||||
TypeInfo::PartialName(start, end) => Self::partial(type_name, *start, Some(*end)),
|
||||
}
|
||||
}
|
||||
|
||||
fn partial(type_name: &'static str, start: isize, end: Option<isize>) -> &'static str {
|
||||
let maxlen = type_name.len();
|
||||
let mut segments = Vec::new();
|
||||
let mut segment_start = 0; // Start position of the current segment.
|
||||
let mut angle_brackets = 0; // Counter for tracking '<' and '>'.
|
||||
let mut previous_char = '\0'; // Initializes to a null character, no previous character.
|
||||
|
||||
for (idx, c) in type_name.char_indices() {
|
||||
match c {
|
||||
':' if angle_brackets == 0 => {
|
||||
if previous_char == ':' {
|
||||
if segment_start < idx - 1 {
|
||||
segments.push((segment_start, idx - 1)); // Do not include last '::'.
|
||||
}
|
||||
segment_start = idx + 1; // Next segment starts after '::'.
|
||||
}
|
||||
}
|
||||
'<' => angle_brackets += 1,
|
||||
'>' => angle_brackets -= 1,
|
||||
_ => {}
|
||||
}
|
||||
previous_char = c;
|
||||
}
|
||||
|
||||
// Include the last segment if there's any.
|
||||
if segment_start < maxlen {
|
||||
segments.push((segment_start, maxlen));
|
||||
}
|
||||
|
||||
// Calculates the start position.
|
||||
let start_pos = segments
|
||||
.get(if start >= 0 {
|
||||
start as usize
|
||||
} else {
|
||||
segments.len() - start.unsigned_abs()
|
||||
})
|
||||
.map_or(0, |&(s, _)| s);
|
||||
|
||||
// Calculates the end position.
|
||||
let end_pos = segments
|
||||
.get(if let Some(end) = end {
|
||||
if end >= 0 {
|
||||
end as usize
|
||||
} else {
|
||||
segments.len() - end.unsigned_abs()
|
||||
}
|
||||
} else {
|
||||
segments.len() - 1
|
||||
})
|
||||
.map_or(maxlen, |&(_, e)| e);
|
||||
|
||||
// Returns the partial string based on the calculated positions.
|
||||
&type_name[start_pos..end_pos]
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates the absolute directory given a root path and a relative path.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `root_path` - A string slice that holds the root path.
|
||||
/// * `relative_path` - A string slice that holds the relative path.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok` - If the operation is successful, returns the absolute directory as a `String`.
|
||||
/// * `Err` - If an I/O error occurs, returns an `io::Error`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
/// - The root path or relative path are invalid.
|
||||
/// - There is an issue with file system operations, such as reading the directory.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let root = "/home/user";
|
||||
/// let relative = "documents";
|
||||
/// let abs_dir = absolute_dir(root, relative).unwrap();
|
||||
/// println!("{}", abs_dir);
|
||||
/// ```
|
||||
pub fn absolute_dir(
|
||||
root_path: impl Into<String>,
|
||||
relative_path: impl Into<String>,
|
||||
) -> Result<String, io::Error> {
|
||||
let root_path = PathBuf::from(root_path.into());
|
||||
let full_path = root_path.join(relative_path.into());
|
||||
let absolute_dir = full_path.to_string_lossy().into();
|
||||
|
||||
if !full_path.is_absolute() {
|
||||
let message = format!("Path \"{absolute_dir}\" is not absolute");
|
||||
trace::warn!(message);
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, message));
|
||||
}
|
||||
|
||||
if !full_path.exists() {
|
||||
let message = format!("Path \"{absolute_dir}\" does not exist");
|
||||
trace::warn!(message);
|
||||
return Err(io::Error::new(io::ErrorKind::NotFound, message));
|
||||
}
|
||||
|
||||
if !full_path.is_dir() {
|
||||
let message = format!("Path \"{absolute_dir}\" is not a directory");
|
||||
trace::warn!(message);
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidInput, message));
|
||||
}
|
||||
|
||||
Ok(absolute_dir)
|
||||
}
|
||||
|
||||
// *************************************************************************************************
|
||||
// MACRO HELPERS.
|
||||
// *************************************************************************************************
|
||||
|
||||
#[macro_export]
|
||||
/// Macro para construir grupos de pares clave-valor.
|
||||
///
|
||||
/// ```rust#ignore
|
||||
/// let args = kv![
|
||||
/// "userName" => "Roberto",
|
||||
/// "photoCount" => 3,
|
||||
/// "userGender" => "male",
|
||||
/// ];
|
||||
/// ```
|
||||
macro_rules! kv {
|
||||
( $($key:expr => $value:expr),* $(,)? ) => {{
|
||||
let mut a = std::collections::HashMap::new();
|
||||
$(
|
||||
a.insert($key.into(), $value.into());
|
||||
)*
|
||||
a
|
||||
}};
|
||||
}
|
||||
130
packages/pagetop/src/lib.rs
Normal file
130
packages/pagetop/src/lib.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
//! <div align="center">
|
||||
//!
|
||||
//! <img src="https://raw.githubusercontent.com/manuelcillero/pagetop/main/static/banner.png" />
|
||||
//!
|
||||
//! <h1>PageTop</h1>
|
||||
//!
|
||||
//! <p>An opinionated web framework to build modular <em>Server-Side Rendering</em> web solutions.</p>
|
||||
//!
|
||||
//! [](https://github.com/manuelcillero/pagetop#-license)
|
||||
//! [](https://docs.rs/pagetop)
|
||||
//! [](https://crates.io/crates/pagetop)
|
||||
//! [](https://crates.io/crates/pagetop)
|
||||
//!
|
||||
//! <br>
|
||||
//! </div>
|
||||
//!
|
||||
//! The `PageTop` core API provides a comprehensive toolkit for extending its functionalities to
|
||||
//! specific requirements and application scenarios through actions, components, packages, and
|
||||
//! themes:
|
||||
//!
|
||||
//! * **Actions** serve as a mechanism to customize `PageTop`'s internal behavior by intercepting
|
||||
//! its execution flow.
|
||||
//! * **Components** encapsulate HTML, CSS, and JavaScript into functional, configurable, and
|
||||
//! well-defined units.
|
||||
//! * **Packages** extend or customize existing functionality by interacting with `PageTop` APIs
|
||||
//! or third-party package APIs.
|
||||
//! * **Themes** enable developers to alter the appearance of pages and components without
|
||||
//! affecting their functionality.
|
||||
//!
|
||||
//! # ⚡️ Quick start
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pagetop::prelude::*;
|
||||
//!
|
||||
//! struct HelloWorld;
|
||||
//!
|
||||
//! impl PackageTrait for HelloWorld {
|
||||
//! fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
//! scfg.route("/", service::web::get().to(hello_world));
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
//! Page::new(request)
|
||||
//! .with_component(Html::with(html! { h1 { "Hello World!" } }))
|
||||
//! .render()
|
||||
//! }
|
||||
//!
|
||||
//! #[pagetop::main]
|
||||
//! async fn main() -> std::io::Result<()> {
|
||||
//! Application::prepare(&HelloWorld).run()?.await
|
||||
//! }
|
||||
//! ```
|
||||
//! This program implements a package named `HelloWorld` with one service that returns a web page
|
||||
//! that greets the world whenever it is accessed from the browser at `http://localhost:8088` (using
|
||||
//! the [default configuration settings](`config::Server`)). You can find this code in the `PageTop`
|
||||
//! [examples repository](https://github.com/manuelcillero/pagetop/tree/latest/examples).
|
||||
//!
|
||||
//! # 🧩 Dependency Management
|
||||
//!
|
||||
//! Projects leveraging `PageTop` will use `cargo` to resolve dependencies, similar to any other
|
||||
//! Rust project.
|
||||
//!
|
||||
//! Nevertheless, it’s crucial that each package explicitly declares its
|
||||
//! [dependencies](core::package::PackageTrait#method.dependencies), if any, to assist `PageTop` in
|
||||
//! structuring and initializing the application in a modular fashion.
|
||||
//!
|
||||
//! # 🚧 Warning
|
||||
//!
|
||||
//! **`PageTop`** framework is currently in active development. The API is unstable and subject to
|
||||
//! frequent changes. Production use is not recommended until version **0.1.0**.
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
// *************************************************************************************************
|
||||
// RE-EXPORTED MACROS AND DERIVES.
|
||||
// *************************************************************************************************
|
||||
|
||||
pub use concat_string::concat_string as join;
|
||||
|
||||
/// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers.
|
||||
pub use paste::paste;
|
||||
|
||||
pub use pagetop_macros::{main, test, AutoDefault};
|
||||
|
||||
// *************************************************************************************************
|
||||
// GLOBAL.
|
||||
// *************************************************************************************************
|
||||
|
||||
pub use static_files::Resource as StaticResource;
|
||||
|
||||
pub type HashMapResources = std::collections::HashMap<&'static str, StaticResource>;
|
||||
|
||||
pub use std::any::TypeId;
|
||||
|
||||
pub type Weight = i8;
|
||||
|
||||
// Global settings, functions and macro helpers.
|
||||
pub mod global;
|
||||
|
||||
static_locales!(LOCALES_PAGETOP);
|
||||
|
||||
// *************************************************************************************************
|
||||
// PUBLIC API.
|
||||
// *************************************************************************************************
|
||||
|
||||
// Retrieve and apply settings values from configuration files.
|
||||
pub mod config;
|
||||
// Application tracing and event logging.
|
||||
pub mod trace;
|
||||
// Localization.
|
||||
pub mod locale;
|
||||
|
||||
// Essential web framework.
|
||||
pub mod service;
|
||||
|
||||
// Key types and functions for creating actions, components, packages, and themes.
|
||||
pub mod core;
|
||||
|
||||
// Web request response variants.
|
||||
pub mod response;
|
||||
|
||||
// Prepare and run the application.
|
||||
pub mod app;
|
||||
|
||||
// *************************************************************************************************
|
||||
// The PageTop Prelude.
|
||||
// *************************************************************************************************
|
||||
|
||||
pub mod prelude;
|
||||
285
packages/pagetop/src/locale.rs
Normal file
285
packages/pagetop/src/locale.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
//! Localization (L10n).
|
||||
//!
|
||||
//! PageTop uses the [Fluent](https://www.projectfluent.org/) set of specifications for application
|
||||
//! localization.
|
||||
//!
|
||||
//! # Fluent Syntax (FTL)
|
||||
//!
|
||||
//! The format used to describe the translation resources used by Fluent is called
|
||||
//! [FTL](https://www.projectfluent.org/fluent/guide/). FTL is designed to be easy to read while
|
||||
//! simultaneously allowing the representation of complex natural language concepts to address
|
||||
//! gender, plurals, conjugations, and others.
|
||||
//!
|
||||
//! # Fluent Resources
|
||||
//!
|
||||
//! PageTop utilizes [fluent-templates](https://docs.rs/fluent-templates/) to integrate localization
|
||||
//! resources into the application binary. The following example groups files and subfolders from
|
||||
//! *src/locale* that have a valid [Unicode Language Identifier](https://docs.rs/unic-langid/) and
|
||||
//! assigns them to their corresponding identifier:
|
||||
//!
|
||||
//! ```text
|
||||
//! src/locale/
|
||||
//! ├── common.ftl
|
||||
//! ├── en-US/
|
||||
//! │ ├── default.ftl
|
||||
//! │ └── main.ftl
|
||||
//! ├── es-ES/
|
||||
//! │ ├── default.ftl
|
||||
//! │ └── main.ftl
|
||||
//! ├── es-MX/
|
||||
//! │ ├── default.ftl
|
||||
//! │ └── main.ftl
|
||||
//! └── fr/
|
||||
//! ├── default.ftl
|
||||
//! └── main.ftl
|
||||
//! ```
|
||||
//!
|
||||
//! Example of a file *src/locale/en-US/main.ftl*:
|
||||
//!
|
||||
//! ```text
|
||||
//! hello-world = Hello world!
|
||||
//! hello-user = Hello, {$userName}!
|
||||
//! shared-photos =
|
||||
//! {$userName} {$photoCount ->
|
||||
//! [one] added a new photo
|
||||
//! *[other] added {$photoCount} new photos
|
||||
//! } of {$userGender ->
|
||||
//! [male] him and his family
|
||||
//! [female] her and her family
|
||||
//! *[other] the family
|
||||
//! }.
|
||||
//! ```
|
||||
//!
|
||||
//! Example of the equivalent file *src/locale/es-ES/main.ftl*:
|
||||
//!
|
||||
//! ```text
|
||||
//! hello-world = Hola mundo!
|
||||
//! hello-user = ¡Hola, {$userName}!
|
||||
//! shared-photos =
|
||||
//! {$userName} {$photoCount ->
|
||||
//! [one] ha añadido una nueva foto
|
||||
//! *[other] ha añadido {$photoCount} nuevas fotos
|
||||
//! } de {$userGender ->
|
||||
//! [male] él y su familia
|
||||
//! [female] ella y su familia
|
||||
//! *[other] la familia
|
||||
//! }.
|
||||
//! ```
|
||||
//!
|
||||
//! # How to apply localization in your code
|
||||
//!
|
||||
//! Once you have created your FTL resource directory, use the
|
||||
//! [`static_locales!`](crate::static_locales) macro to integrate them into your module or
|
||||
//! application. If your resources are located in the `"src/locale"` directory, simply declare:
|
||||
//!
|
||||
//! ```
|
||||
//! use pagetop::prelude::*;
|
||||
//!
|
||||
//! static_locales!(LOCALES_SAMPLE);
|
||||
//! ```
|
||||
//!
|
||||
//! But if they are in another directory, then you can use:
|
||||
//!
|
||||
//! ```
|
||||
//! use pagetop::prelude::*;
|
||||
//!
|
||||
//! static_locales!(LOCALES_SAMPLE in "path/to/locale");
|
||||
//! ```
|
||||
|
||||
use crate::{global, kv, AutoDefault, LOCALES_PAGETOP};
|
||||
|
||||
pub use fluent_templates;
|
||||
pub use unic_langid::LanguageIdentifier;
|
||||
|
||||
use fluent_templates::Loader;
|
||||
use fluent_templates::StaticLoader as Locales;
|
||||
|
||||
use unic_langid::langid;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
const LANGUAGE_SET_FAILURE: &str = "language_set_failure";
|
||||
|
||||
static LANGUAGES: LazyLock<HashMap<String, (LanguageIdentifier, &str)>> = LazyLock::new(|| {
|
||||
kv![
|
||||
"en" => (langid!("en-US"), "English"),
|
||||
"en-GB" => (langid!("en-GB"), "English (British)"),
|
||||
"en-US" => (langid!("en-US"), "English (United States)"),
|
||||
"es" => (langid!("es-ES"), "Spanish"),
|
||||
"es-ES" => (langid!("es-ES"), "Spanish (Spain)"),
|
||||
]
|
||||
});
|
||||
|
||||
pub static LANGID_FALLBACK: LazyLock<LanguageIdentifier> = LazyLock::new(|| langid!("en-US"));
|
||||
|
||||
/// Sets the application's default
|
||||
/// [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier)
|
||||
/// through `SETTINGS.app.language`.
|
||||
pub static LANGID_DEFAULT: LazyLock<&LanguageIdentifier> = LazyLock::new(|| {
|
||||
langid_for(global::SETTINGS.app.language.as_str()).unwrap_or(&LANGID_FALLBACK)
|
||||
});
|
||||
|
||||
pub fn langid_for(language: impl Into<String>) -> Result<&'static LanguageIdentifier, String> {
|
||||
let language = language.into();
|
||||
match LANGUAGES.get(language.as_str()) {
|
||||
Some((langid, _)) => Ok(langid),
|
||||
None => {
|
||||
if language.is_empty() {
|
||||
Ok(&LANGID_FALLBACK)
|
||||
} else {
|
||||
Err(format!(
|
||||
"No langid for Unicode Language Identifier \"{language}\".",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
/// Defines a set of localization elements and local translation texts.
|
||||
macro_rules! static_locales {
|
||||
( $LOCALES:ident $(, $core_locales:literal)? ) => {
|
||||
$crate::locale::fluent_templates::static_loader! {
|
||||
static $LOCALES = {
|
||||
locales: "src/locale",
|
||||
$( core_locales: $core_locales, )?
|
||||
fallback_language: "en-US",
|
||||
// Removes unicode isolating marks around arguments.
|
||||
customise: |bundle| bundle.set_use_isolating(false),
|
||||
};
|
||||
}
|
||||
};
|
||||
( $LOCALES:ident in $dir_locales:literal $(, $core_locales:literal)? ) => {
|
||||
$crate::locale::fluent_templates::static_loader! {
|
||||
static $LOCALES = {
|
||||
locales: $dir_locales,
|
||||
$( core_locales: $core_locales, )?
|
||||
fallback_language: "en-US",
|
||||
// Removes unicode isolating marks around arguments.
|
||||
customise: |bundle| bundle.set_use_isolating(false),
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
enum L10nOp {
|
||||
#[default]
|
||||
None,
|
||||
Text(String),
|
||||
Translate(String),
|
||||
}
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
pub struct L10n {
|
||||
op: L10nOp,
|
||||
locales: Option<&'static Locales>,
|
||||
args: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl L10n {
|
||||
pub fn none() -> Self {
|
||||
L10n::default()
|
||||
}
|
||||
|
||||
pub fn n(text: impl Into<String>) -> Self {
|
||||
L10n {
|
||||
op: L10nOp::Text(text.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn l(key: impl Into<String>) -> Self {
|
||||
L10n {
|
||||
op: L10nOp::Translate(key.into()),
|
||||
locales: Some(&LOCALES_PAGETOP),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn t(key: impl Into<String>, locales: &'static Locales) -> Self {
|
||||
L10n {
|
||||
op: L10nOp::Translate(key.into()),
|
||||
locales: Some(locales),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_arg(mut self, arg: impl Into<String>, value: impl Into<String>) -> Self {
|
||||
self.args.insert(arg.into(), value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn using(&self, langid: &LanguageIdentifier) -> Option<String> {
|
||||
match &self.op {
|
||||
L10nOp::None => None,
|
||||
L10nOp::Text(text) => Some(text.to_owned()),
|
||||
L10nOp::Translate(key) => match self.locales {
|
||||
Some(locales) => {
|
||||
if self.args.is_empty() {
|
||||
locales.try_lookup(langid, key)
|
||||
} else {
|
||||
locales.try_lookup_with_args(
|
||||
langid,
|
||||
key,
|
||||
&self
|
||||
.args
|
||||
.iter()
|
||||
.fold(HashMap::new(), |mut args, (key, value)| {
|
||||
args.insert(key.to_string(), value.to_owned().into());
|
||||
args
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for L10n {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.op {
|
||||
L10nOp::None => write!(f, ""),
|
||||
L10nOp::Text(text) => write!(f, "{text}"),
|
||||
L10nOp::Translate(key) => {
|
||||
if let Some(locales) = self.locales {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
if self.args.is_empty() {
|
||||
locales.lookup(
|
||||
match key.as_str() {
|
||||
LANGUAGE_SET_FAILURE => &LANGID_FALLBACK,
|
||||
_ => &LANGID_DEFAULT,
|
||||
},
|
||||
key,
|
||||
)
|
||||
} else {
|
||||
locales.lookup_with_args(
|
||||
match key.as_str() {
|
||||
LANGUAGE_SET_FAILURE => &LANGID_FALLBACK,
|
||||
_ => &LANGID_DEFAULT,
|
||||
},
|
||||
key,
|
||||
&self
|
||||
.args
|
||||
.iter()
|
||||
.fold(HashMap::new(), |mut args, (key, value)| {
|
||||
args.insert(key.to_string(), value.to_owned().into());
|
||||
args
|
||||
}),
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
write!(f, "Unknown localization {key}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
packages/pagetop/src/locale/en-US/base.ftl
Normal file
13
packages/pagetop/src/locale/en-US/base.ftl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Branding component.
|
||||
site_home = Home
|
||||
|
||||
# PoweredBy component.
|
||||
poweredby_pagetop = Powered by {$pagetop_link}
|
||||
pagetop_logo = PageTop logo
|
||||
|
||||
# Menu component.
|
||||
menu_toggle = Toggle menu visibility
|
||||
|
||||
# Form components.
|
||||
button_submit = Submit
|
||||
button_reset = Reset
|
||||
8
packages/pagetop/src/locale/en-US/theme.ftl
Normal file
8
packages/pagetop/src/locale/en-US/theme.ftl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
header = Header
|
||||
pagetop = Page Top
|
||||
content = Content
|
||||
sidebar_left = Sidebar Left
|
||||
sidebar_right = Sidebar Right
|
||||
footer = Footer
|
||||
|
||||
skip_to_content = Skip to main content (Press Enter)
|
||||
26
packages/pagetop/src/locale/en-US/welcome.ftl
Normal file
26
packages/pagetop/src/locale/en-US/welcome.ftl
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
welcome_package_name = Default homepage
|
||||
welcome_package_description = Displays a demo homepage when none is configured.
|
||||
|
||||
welcome_title = Hello world!
|
||||
|
||||
welcome_intro = This page is used to check the proper operation of the { $app } installation.
|
||||
welcome_powered = This web solution is powered by { $pagetop }.
|
||||
welcome_code = Code
|
||||
welcome = Welcome
|
||||
|
||||
welcome_page = Welcome page
|
||||
welcome_subtitle = Are you user of { $app }?
|
||||
welcome_text1 = If you don't know what this page is about, this probably means that the site is either experiencing problems or is undergoing routine maintenance.
|
||||
welcome_text2 = If the problem persists, please contact your system administrator.
|
||||
|
||||
welcome_pagetop_title = About PageTop
|
||||
welcome_pagetop_text1 = If you can read this page, it means that the PageTop server is working properly, but has not yet been configured.
|
||||
welcome_pagetop_text2 = PageTop is a <a href="https://www.rust-lang.org" target="_blank">Rust</a>-based web development framework to build modular, extensible, and configurable web solutions.
|
||||
welcome_pagetop_text3 = For more information on PageTop please visit the <a href="https://docs.rs/pagetop/latest/pagetop" target="_blank">technical documentation</a>.
|
||||
|
||||
welcome_promo_title = Promoting PageTop
|
||||
welcome_promo_text1 = You are free to use the image below on applications powered by { $pagetop }. Thanks for using PageTop!
|
||||
|
||||
welcome_issues_title = Reporting problems
|
||||
welcome_issues_text1 = Please use <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank">GitHub to report any issues</a> with PageTop. However, check the existing error reports before submitting a new issue.
|
||||
welcome_issues_text2 = If the issues are specific to { $app }, please refer to its official repository or support channel, rather than directly to PageTop.
|
||||
13
packages/pagetop/src/locale/es-ES/base.ftl
Normal file
13
packages/pagetop/src/locale/es-ES/base.ftl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Branding component.
|
||||
site_home = Inicio
|
||||
|
||||
# PoweredBy component.
|
||||
poweredby_pagetop = Funciona con {$pagetop_link}
|
||||
pagetop_logo = Logotipo de PageTop
|
||||
|
||||
# Menu component.
|
||||
menu_toggle = Alternar visibilidad del menú
|
||||
|
||||
# Form components.
|
||||
button_submit = Enviar
|
||||
button_reset = Reiniciar
|
||||
8
packages/pagetop/src/locale/es-ES/theme.ftl
Normal file
8
packages/pagetop/src/locale/es-ES/theme.ftl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
header = Cabecera
|
||||
pagetop = Superior
|
||||
content = Contenido
|
||||
sidebar_left = Barra lateral izquierda
|
||||
sidebar_right = Barra lateral derecha
|
||||
footer = Pie
|
||||
|
||||
skip_to_content = Ir al contenido principal (Pulsar Intro)
|
||||
26
packages/pagetop/src/locale/es-ES/welcome.ftl
Normal file
26
packages/pagetop/src/locale/es-ES/welcome.ftl
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
welcome_package_name = Página de inicio predeterminada
|
||||
welcome_package_description = Muestra una página de demostración predeterminada cuando no hay ninguna configurada.
|
||||
|
||||
welcome_title = ¡Hola mundo!
|
||||
|
||||
welcome_intro = Esta página se utiliza para verificar el correcto funcionamiento de la instalación de { $app }.
|
||||
welcome_powered = Esta solución web funciona con { $pagetop }.
|
||||
welcome_code = Código
|
||||
welcome = Bienvenida
|
||||
|
||||
welcome_page = Página de bienvenida
|
||||
welcome_subtitle = ¿Eres usuario de { $app }?
|
||||
welcome_text1 = Si no sabes de qué trata esta página, probablemente significa que el sitio está experimentando problemas o está pasando por un mantenimiento de rutina.
|
||||
welcome_text2 = Si el problema persiste, póngase en contacto con el administrador del sistema.
|
||||
|
||||
welcome_pagetop_title = Sobre PageTop
|
||||
welcome_pagetop_text1 = Si puedes leer esta página, significa que el servidor PageTop funciona correctamente, pero aún no se ha configurado.
|
||||
welcome_pagetop_text2 = PageTop es un entorno de desarrollo web basado en <a href="https://www.rust-lang.org/es" target="_blank">Rust</a> para construir soluciones web modulares, extensibles y configurables.
|
||||
welcome_pagetop_text3 = Para más información sobre PageTop, por favor visita la <a href="https://docs.rs/pagetop/latest/pagetop" target="_blank">documentación técnica</a>.
|
||||
|
||||
welcome_promo_title = Promociona PageTop
|
||||
welcome_promo_text1 = Eres libre de usar la siguiente imagen en aplicaciones desarrolladas con { $pagetop }. ¡Gracias por usar PageTop!
|
||||
|
||||
welcome_issues_title = Informando problemas
|
||||
welcome_issues_text1 = Por favor, utiliza <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank">GitHub para reportar cualquier problema</a> con PageTop. No obstante, comprueba los informes de errores existentes antes de enviar uno nuevo.
|
||||
welcome_issues_text2 = Si son fallos específicos de { $app }, por favor acude a su repositorio o canal de soporte oficial y no al de PageTop directamente.
|
||||
38
packages/pagetop/src/prelude.rs
Normal file
38
packages/pagetop/src/prelude.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
//! The `PageTop` Prelude.
|
||||
|
||||
// RE-EXPORTED.
|
||||
pub use crate::{join, main, paste, test, AutoDefault};
|
||||
|
||||
// GLOBAL.
|
||||
pub use crate::{global, HashMapResources, TypeId, Weight};
|
||||
|
||||
// MACROS.
|
||||
|
||||
// crate::global
|
||||
pub use crate::kv;
|
||||
// crate::config
|
||||
pub use crate::config_defaults;
|
||||
// crate::locale
|
||||
pub use crate::static_locales;
|
||||
// crate::service
|
||||
pub use crate::{static_files, static_files_service};
|
||||
// crate::core::action
|
||||
pub use crate::actions;
|
||||
|
||||
// API.
|
||||
|
||||
pub use crate::trace;
|
||||
|
||||
pub use crate::locale::*;
|
||||
|
||||
pub use crate::service;
|
||||
pub use crate::service::{HttpMessage, HttpRequest};
|
||||
|
||||
pub use crate::core::{AnyBase, AnyTo};
|
||||
|
||||
pub use crate::core::action::*;
|
||||
pub use crate::core::package::*;
|
||||
|
||||
pub use crate::response::{json::*, redirect::*, ResponseError};
|
||||
|
||||
pub use crate::app::Application;
|
||||
7
packages/pagetop/src/response.rs
Normal file
7
packages/pagetop/src/response.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//! Web request response variants.
|
||||
|
||||
pub use actix_web::ResponseError;
|
||||
|
||||
pub mod json;
|
||||
|
||||
pub mod redirect;
|
||||
1
packages/pagetop/src/response/json.rs
Normal file
1
packages/pagetop/src/response/json.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub use actix_web::web::Json;
|
||||
76
packages/pagetop/src/response/redirect.rs
Normal file
76
packages/pagetop/src/response/redirect.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//! Perform redirections in HTTP.
|
||||
//!
|
||||
//! **URL redirection**, also known as *URL forwarding*, is a technique to give more than one URL
|
||||
//! address to a web resource. HTTP has a response called ***HTTP redirect*** for this operation
|
||||
//! (see [Redirections in HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections)).
|
||||
//!
|
||||
//! There are several types of redirects, sorted into three categories:
|
||||
//!
|
||||
//! * **Permanent redirections**. These redirections are meant to last forever. They imply that
|
||||
//! the original URL should no longer be used, and replaced with the new one. Search engine
|
||||
//! robots, RSS readers, and other crawlers will update the original URL for the resource.
|
||||
//!
|
||||
//! * **Temporary redirections**. Sometimes the requested resource can't be accessed from its
|
||||
//! canonical location, but it can be accessed from another place. In this case, a temporary
|
||||
//! redirect can be used. Search engine robots and other crawlers don't memorize the new,
|
||||
//! temporary URL. Temporary redirections are also used when creating, updating, or deleting
|
||||
//! resources, to show temporary progress pages.
|
||||
//!
|
||||
//! * **Special redirections**.
|
||||
|
||||
use crate::service::HttpResponse;
|
||||
|
||||
pub struct Redirect;
|
||||
|
||||
impl Redirect {
|
||||
/// Permanent redirection. Status Code **301**. GET methods unchanged. Others may or may not be
|
||||
/// changed to GET. Typical for reorganization of a website.
|
||||
pub fn moved(redirect_to_url: &str) -> HttpResponse {
|
||||
HttpResponse::MovedPermanently()
|
||||
.append_header(("Location", redirect_to_url))
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Permanent redirection. Status Code **308**. Method and body not changed. Typical for
|
||||
/// reorganization of a website, with non-GET links/operations.
|
||||
pub fn permanent(redirect_to_url: &str) -> HttpResponse {
|
||||
HttpResponse::PermanentRedirect()
|
||||
.append_header(("Location", redirect_to_url))
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Temporary redirection. Status Code **302**. GET methods unchanged. Others may or may not be
|
||||
/// changed to GET. Used when the web page is temporarily unavailable for unforeseen reasons.
|
||||
pub fn found(redirect_to_url: &str) -> HttpResponse {
|
||||
HttpResponse::Found()
|
||||
.append_header(("Location", redirect_to_url))
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Temporary redirection. Status Code **303**. GET methods unchanged. Others changed to GET
|
||||
/// (body lost). Used to redirect after a PUT or a POST, so that refreshing the result page
|
||||
/// doesn't re-trigger the operation.
|
||||
pub fn see_other(redirect_to_url: &str) -> HttpResponse {
|
||||
HttpResponse::SeeOther()
|
||||
.append_header(("Location", redirect_to_url))
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Temporary redirection. Status Code **307**. Method and body not changed. The web page is
|
||||
/// temporarily unavailable for unforeseen reasons. Better than [`found()`](Self::found) when
|
||||
/// non-GET operations are available on the site.
|
||||
pub fn temporary(redirect_to_url: &str) -> HttpResponse {
|
||||
HttpResponse::TemporaryRedirect()
|
||||
.append_header(("Location", redirect_to_url))
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Special redirection. Status Code **304**. Redirects a page to the locally cached copy (that
|
||||
/// was stale). Sent for revalidated conditional requests. Indicates that the cached response is
|
||||
/// still fresh and can be used.
|
||||
pub fn not_modified(redirect_to_url: &str) -> HttpResponse {
|
||||
HttpResponse::NotModified()
|
||||
.append_header(("Location", redirect_to_url))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
63
packages/pagetop/src/service.rs
Normal file
63
packages/pagetop/src/service.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
//! Essential web framework ([actix-web](https://docs.rs/actix-web)).
|
||||
|
||||
pub use actix_session::Session;
|
||||
pub use actix_web::body::BoxBody;
|
||||
pub use actix_web::dev::Server;
|
||||
pub use actix_web::dev::ServiceFactory as Factory;
|
||||
pub use actix_web::dev::ServiceRequest as Request;
|
||||
pub use actix_web::dev::ServiceResponse as Response;
|
||||
pub use actix_web::{cookie, get, http, rt, test, web};
|
||||
pub use actix_web::{App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer, Responder};
|
||||
|
||||
pub use actix_web_files::Files as ActixFiles;
|
||||
pub use actix_web_static_files::ResourceFiles;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! static_files {
|
||||
( $bundle:ident ) => {
|
||||
$crate::paste! {
|
||||
mod [<static_files_ $bundle>] {
|
||||
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
|
||||
}
|
||||
}
|
||||
};
|
||||
( $bundle:ident => $STATIC:ident ) => {
|
||||
$crate::paste! {
|
||||
mod [<static_files_ $bundle>] {
|
||||
include!(concat!(env!("OUT_DIR"), "/", stringify!($bundle), ".rs"));
|
||||
}
|
||||
static $STATIC: std::sync::LazyLock<HashMapResources> = std::sync::LazyLock::new(
|
||||
[<static_files_ $bundle>]::$bundle
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! static_files_service {
|
||||
( $scfg:ident, $bundle:ident => $path:expr $(, [$root:expr, $relative:expr])? ) => {{
|
||||
$crate::paste! {
|
||||
let span = $crate::trace::debug_span!("Configuring static files ", path = $path);
|
||||
let _ = span.in_scope(|| {
|
||||
let mut serve_embedded:bool = true;
|
||||
$(
|
||||
if !$root.is_empty() && !$relative.is_empty() {
|
||||
if let Ok(absolute) = $crate::global::absolute_dir($root, $relative) {
|
||||
$scfg.service($crate::service::ActixFiles::new(
|
||||
$path,
|
||||
absolute,
|
||||
).show_files_listing());
|
||||
serve_embedded = false
|
||||
}
|
||||
}
|
||||
)?
|
||||
if serve_embedded {
|
||||
$scfg.service($crate::service::ResourceFiles::new(
|
||||
$path,
|
||||
[<static_files_ $bundle>]::$bundle(),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
}};
|
||||
}
|
||||
84
packages/pagetop/src/trace.rs
Normal file
84
packages/pagetop/src/trace.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
//! Application tracing and event logging.
|
||||
//!
|
||||
//! `PageTop` collects application diagnostic information in a structured and event-based manner.
|
||||
//!
|
||||
//! In asynchronous systems, interpreting traditional log messages often becomes complicated.
|
||||
//! Individual tasks are multiplexed to the same thread, and associated events and log messages get
|
||||
//! intermingled, making it difficult to follow the logical sequence.
|
||||
//!
|
||||
//! `PageTop` uses [`tracing`](https://docs.rs/tracing) to allow **applications** and **modules** to
|
||||
//! log structured events with added information about *temporality* and *causality*. Unlike a log
|
||||
//! message, a span has a start and end time, can enter and exit the execution flow, and can exist
|
||||
//! within a nested tree of similar spans. Additionally, these spans are *structured*, with the
|
||||
//! ability to record data types and text messages.
|
||||
|
||||
use crate::global;
|
||||
|
||||
pub use tracing::{debug, error, info, trace, warn};
|
||||
pub use tracing::{debug_span, error_span, info_span, trace_span, warn_span};
|
||||
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// Application tracing and event logging.
|
||||
///
|
||||
/// To increase performance, a dedicated thread uses a non-blocking writer system that acts
|
||||
/// periodically instead of sending each trace or event instantly. If the program terminates
|
||||
/// abruptly (e.g., due to a panic! or a `std::process::exit`), some traces or events might not be
|
||||
/// sent.
|
||||
///
|
||||
/// Since traces or events logged shortly before an application crash are often important for
|
||||
/// diagnosing the cause of the failure, `Lazy<WorkerGuard>` ensures that all stored logs are sent
|
||||
/// before terminating execution.
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub(crate) static TRACING: LazyLock<WorkerGuard> = LazyLock::new(|| {
|
||||
let env_filter = EnvFilter::try_new(&global::SETTINGS.log.tracing)
|
||||
.unwrap_or_else(|_| EnvFilter::new("Info"));
|
||||
|
||||
let rolling = global::SETTINGS.log.rolling.to_lowercase();
|
||||
|
||||
let (non_blocking, guard) = match rolling.as_str() {
|
||||
"stdout" => tracing_appender::non_blocking(std::io::stdout()),
|
||||
_ => tracing_appender::non_blocking({
|
||||
let path = &global::SETTINGS.log.path;
|
||||
let prefix = &global::SETTINGS.log.prefix;
|
||||
match rolling.as_str() {
|
||||
"daily" => tracing_appender::rolling::daily(path, prefix),
|
||||
"hourly" => tracing_appender::rolling::hourly(path, prefix),
|
||||
"minutely" => tracing_appender::rolling::minutely(path, prefix),
|
||||
"endless" => tracing_appender::rolling::never(path, prefix),
|
||||
_ => {
|
||||
println!(
|
||||
"Rolling value \"{}\" not valid. Using \"daily\". Check the settings file.",
|
||||
global::SETTINGS.log.rolling,
|
||||
);
|
||||
tracing_appender::rolling::daily(path, prefix)
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter(env_filter)
|
||||
.with_writer(non_blocking)
|
||||
.with_ansi(rolling.as_str() == "stdout");
|
||||
|
||||
match global::SETTINGS.log.format.to_lowercase().as_str() {
|
||||
"json" => subscriber.json().init(),
|
||||
"full" => subscriber.init(),
|
||||
"compact" => subscriber.compact().init(),
|
||||
"pretty" => subscriber.pretty().init(),
|
||||
_ => {
|
||||
println!(
|
||||
"Tracing format \"{}\" not valid. Using \"Full\". Check the settings file.",
|
||||
global::SETTINGS.log.format,
|
||||
);
|
||||
subscriber.init();
|
||||
}
|
||||
}
|
||||
|
||||
guard
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue