♻️ Restructure dirs/files to fix examples build

This commit is contained in:
Manuel Cillero 2024-11-20 00:15:39 +01:00
parent 92268ca653
commit 73ba80eff7
53 changed files with 47 additions and 43 deletions

View file

@ -1,49 +0,0 @@
[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 }
authors = { workspace = true }
license = { workspace = true }
[lib]
name = "pagetop"
[dependencies]
colored = "2.1.0"
concat-string = "1.0.1"
figlet-rs = "0.1.5"
itoa = "1.0.11"
nom = "7.1.3"
paste = "1.0.15"
substring = "1.4.5"
terminal_size = "0.4.0"
toml = "0.8.19"
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] }
tracing-actix-web = "0.7.15"
fluent-bundle = "0.15.3"
fluent-templates = "0.11.0"
unic-langid = { version = "0.9.5", features = ["macros"] }
actix-web = "4.9.0"
actix-web-files = { package = "actix-files", version = "0.6.6" }
actix-web-static-files = "4.0.1"
actix-session = { version = "0.10.1", features = ["cookie-session"] }
serde.workspace = true
static-files.workspace = true
pagetop-macros.workspace = true

View file

@ -1,165 +0,0 @@
//! 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::DEFAULT_LANGID);
// 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 colored::Colorize;
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_ff = "".to_string();
let app_name = &global::SETTINGS.app.name;
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_ff = ff.to_string();
}
}
}
if app_ff.is_empty() {
println!("\n{app_name}");
} else {
print!("\n{app_ff}");
}
// Application description.
if !global::SETTINGS.app.description.is_empty() {
println!("{}", global::SETTINGS.app.description.cyan());
};
// PageTop version.
println!(
"{} {}\n",
"Powered by PageTop".yellow(),
env!("CARGO_PKG_VERSION").yellow()
);
}
}
/// 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))
}
*/

View file

@ -1,30 +0,0 @@
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()
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,719 +0,0 @@
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 / $@
> < $@
/ . \ $@
/__/ \__\$@
$@@
____ ____$@
\ \ / /$@
\ \/ /$ @
\_ _/$ @
| |$ @
|__|$ @
$ @@
________ $@
| / $@
`---/ / $@
/ / $@
/ /----.@
/________|@
$@@
___@
/ /@
| |$@
/ /$ @
\ \$ @
| |$@
\__\@@
__ $@
| |$@
| |$@
| |$@
| |$@
| |$@
|__|$@@
___ @
\ \$ @
| | @
\ \@
/ /@
| | @
/__/$ @@
__ _ @
/ \/ |@
|_/\__/ @
$ @
$ @
$ @
@@
_ _ @
(_)_(_) @
/ \ @
/ _ \ @
/ ___ \ @
/_/ \_\@
@@
_ _ @
(_)_(_)@
/ _ \ @
| | | |@
| |_| |@
\___/ @
@@
_ _ @
(_) (_)@
| | | |@
| | | |@
| |_| |@
\___/ @
@@
_ _ @
(_) (_)@
__ _ @
/ _` |@
| (_| |@
\__,_|@
@@
_ _ @
(_) (_)@
___ @
/ _ \ @
| (_) |@
\___/ @
@@
_ _ @
(_) (_)@
_ _ @
| | | |@
| |_| |@
\__,_|@
@@
___ @
/ _ \ @
| | ) |@
| |< < @
| | ) |@
| ||_/ @
|_| @@

View file

@ -1,85 +0,0 @@
//! Key types and functions for creating actions, packages, and themes.
use crate::util::TypeInfo;
use std::any::Any;
/// A base trait that extends `Any` to provide metadata and dynamic type casting
/// capabilities.
pub trait AnyBase: Any {
/// Returns the full name of the type.
fn type_name(&self) -> &'static str;
/// Returns a short name for the type.
fn short_name(&self) -> &'static str;
/// Returns a reference to `dyn Any` for dynamic type casting.
fn as_any_ref(&self) -> &dyn Any;
/// Returns a mutable reference to `dyn Any` for dynamic type casting.
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
}
}
/// A trait for advanced dynamic type manipulation and downcasting.
pub trait AnyTo: AnyBase {
/// Checks if the type is of the specified type `T`.
#[inline]
fn is<T>(&self) -> bool
where
T: AnyBase,
{
self.as_any_ref().is::<T>()
}
/// Attempts to downcast a reference to the specified type `T`.
#[inline]
fn downcast_ref<T>(&self) -> Option<&T>
where
T: AnyBase,
{
self.as_any_ref().downcast_ref()
}
/// Attempts to downcast a mutable reference to the specified type `T`.
#[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 modify the predefined behavior of the code.
pub mod action;
// API to add new features with packages.
pub mod package;
// API to add new layouts with themes.
pub mod theme;

View file

@ -1,19 +0,0 @@
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),)+]
}};
}

View file

@ -1,30 +0,0 @@
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);
}
}

View file

@ -1,61 +0,0 @@
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(),
}
}
}

View file

@ -1,43 +0,0 @@
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();
}
}

View file

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

View file

@ -1,128 +0,0 @@
use crate::core::action::add_action;
use crate::core::package::{welcome, PackageRef};
use crate::core::theme::all::THEMES;
use crate::{service, trace};
use std::sync::{LazyLock, RwLock};
// 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();
// 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) {
for m in ENABLED_PACKAGES.read().unwrap().iter() {
m.configure_service(scfg);
}
// Default welcome homepage.
scfg.route("/", service::web::get().to(welcome::homepage));
}

View file

@ -1,39 +0,0 @@
use crate::core::action::ActionBox;
use crate::core::theme::ThemeRef;
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 theme(&self) -> Option<ThemeRef> {
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) {}
}

View file

@ -1,120 +0,0 @@
use crate::html::{html, Markup};
use crate::locale::L10n;
use crate::{concat_string, global};
pub async fn homepage() -> Markup {
html! {
head {
meta charset="UTF-8" {}
meta name="viewport" content="width=device-width, initial-scale=1" {}
title { (concat_string!(
&global::SETTINGS.app.name, " | ", L10n::l("welcome_page").to_string()
)) }
style { r#"
body {
background-color: #f3d060;
font-size: 20px;
}
.wrapper {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 0;
}
.container {
padding: 0 16px;
}
.title {
font-size: clamp(3rem, 10vw, 10rem);
letter-spacing: -0.05em;
line-height: 1.2;
margin: 0;
}
.subtitle {
font-size: clamp(1.8rem, 2vw, 3rem);
letter-spacing: -0.02em;
line-height: 1.2;
margin: 0;
}
.powered {
margin: .5em 0 1em;
}
.box-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: stretch;
gap: 1.5em;
}
.box {
flex: 1 1 280px;
border: 3px solid #25282a;
box-shadow: 5px 5px 0px #25282a;
box-sizing: border-box;
padding: 0 16px;
}
footer {
margin-top: 5em;
font-size: 14px;
font-weight: 500;
color: #a5282c;
}
"# }
}
body style="font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;" {
div class="wrapper" {
div class="container" {
h1 class="title" { (L10n::l("welcome_title").markup()) }
p class="subtitle" {
(L10n::l("welcome_intro").with_arg("app", format!(
"<span style=\"font-weight: bold;\">{}</span>",
&global::SETTINGS.app.name
)).markup())
}
p class="powered" {
(L10n::l("welcome_powered").with_arg("pagetop", format!(
"<a href=\"{}\" target=\"_blank\">{}</a>",
"https://crates.io/crates/pagetop", "PageTop"
)).markup())
}
h2 { (L10n::l("welcome_page").markup()) }
div class="box-container" {
section class="box" style="background-color: #5eb0e5;" {
h3 {
(L10n::l("welcome_subtitle")
.with_arg("app", &global::SETTINGS.app.name)
.markup())
}
p { (L10n::l("welcome_text1").markup()) }
p { (L10n::l("welcome_text2").markup()) }
}
section class="box" style="background-color: #aee1cd;" {
h3 {
(L10n::l("welcome_pagetop_title").markup())
}
p { (L10n::l("welcome_pagetop_text1").markup()) }
p { (L10n::l("welcome_pagetop_text2").markup()) }
p { (L10n::l("welcome_pagetop_text3").markup()) }
}
section class="box" style="background-color: #ebebe3;" {
h3 {
(L10n::l("welcome_issues_title").markup())
}
p { (L10n::l("welcome_issues_text1").markup()) }
p {
(L10n::l("welcome_issues_text2")
.with_arg("app", &global::SETTINGS.app.name)
.markup())
}
}
}
footer { "[" (L10n::l("welcome_have_fun").markup()) "]" }
}
}
}
}
}

View file

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

View file

@ -1,42 +0,0 @@
use crate::core::theme::ThemeRef;
use std::sync::{LazyLock, RwLock};
// THEMES ******************************************************************************************
pub static THEMES: LazyLock<RwLock<Vec<ThemeRef>>> = LazyLock::new(|| RwLock::new(Vec::new()));
/* DEFAULT THEME ***********************************************************************************
pub struct NoTheme;
impl PackageTrait for NoTheme {
fn theme(&self) -> Option<ThemeRef> {
Some(&NoTheme)
}
}
impl ThemeTrait for NoTheme {
}
pub static DEFAULT_THEME: LazyLock<ThemeRef> =
LazyLock::new(|| match theme_by_short_name(&global::SETTINGS.app.theme) {
Some(theme) => theme,
None => &NoTheme,
});
// THEME BY NAME ***********************************************************************************
pub fn theme_by_short_name(short_name: &str) -> Option<ThemeRef> {
let short_name = short_name.to_lowercase();
match THEMES
.read()
.unwrap()
.iter()
.find(|t| t.short_name().to_lowercase() == short_name)
{
Some(theme) => Some(*theme),
_ => None,
}
}
*/

View file

@ -1,104 +0,0 @@
use crate::core::package::PackageTrait;
pub type ThemeRef = &'static dyn ThemeTrait;
/// Los temas deben implementar este "trait".
pub trait ThemeTrait: PackageTrait + Send + Sync {
/*
#[rustfmt::skip]
fn regions(&self) -> Vec<(&'static str, L10n)> {
vec![
("header", L10n::l("header")),
("pagetop", L10n::l("pagetop")),
("sidebar_left", L10n::l("sidebar_left")),
("content", L10n::l("content")),
("sidebar_right", L10n::l("sidebar_right")),
("footer", L10n::l("footer")),
]
}
#[allow(unused_variables)]
fn before_prepare_body(&self, page: &mut Page) {}
fn prepare_body(&self, page: &mut Page) -> PrepareMarkup {
let skip_to_id = page.body_skip_to().get().unwrap_or("content".to_owned());
PrepareMarkup::With(html! {
body id=[page.body_id().get()] class=[page.body_classes().get()] {
@if let Some(skip) = L10n::l("skip_to_content").using(page.context().langid()) {
div class="skip__to_content" {
a href=(concat_string!("#", skip_to_id)) { (skip) }
}
}
(flex::Container::new()
.with_id("body__wrapper")
.with_direction(flex::Direction::Column(BreakPoint::None))
.with_align(flex::Align::Center)
.add_item(flex::Item::region().with_id("header"))
.add_item(flex::Item::region().with_id("pagetop"))
.add_item(
flex::Item::with(
flex::Container::new()
.with_direction(flex::Direction::Row(BreakPoint::None))
.add_item(
flex::Item::region()
.with_id("sidebar_left")
.with_grow(flex::Grow::Is1),
)
.add_item(
flex::Item::region()
.with_id("content")
.with_grow(flex::Grow::Is3),
)
.add_item(
flex::Item::region()
.with_id("sidebar_right")
.with_grow(flex::Grow::Is1),
),
)
.with_id("flex__wrapper"),
)
.add_item(flex::Item::region().with_id("footer"))
.render(page.context()))
}
})
}
fn after_prepare_body(&self, page: &mut Page) {
page.set_assets(AssetsOp::SetFaviconIfNone(
Favicon::new().with_icon("/base/favicon.ico"),
));
}
fn prepare_head(&self, page: &mut Page) -> PrepareMarkup {
let viewport = "width=device-width, initial-scale=1, shrink-to-fit=no";
PrepareMarkup::With(html! {
head {
meta charset="utf-8";
@if let Some(title) = page.title() {
title { (global::SETTINGS.app.name) (" - ") (title) }
} @else {
title { (global::SETTINGS.app.name) }
}
@if let Some(description) = page.description() {
meta name="description" content=(description);
}
meta name="viewport" content=(viewport);
@for (name, content) in page.metadata() {
meta name=(name) content=(content) {}
}
meta http-equiv="X-UA-Compatible" content="IE=edge";
@for (property, content) in page.properties() {
meta property=(property) content=(content) {}
}
(page.context().prepare_assets())
}
})
}
*/
}

View file

@ -1,121 +0,0 @@
//! Global settings.
use crate::static_config;
use serde::Deserialize;
static_config!(SETTINGS: Settings => [
// [app]
"app.name" => "My App",
"app.description" => "Developed with the amazing PageTop framework.",
"app.theme" => "",
"app.language" => "en-US",
"app.text_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,
]);
#[derive(Debug, Deserialize)]
/// Configuration settings for the 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 {
/// The name of the application.
/// Default: *"My App"*.
pub name: String,
/// A brief description of the application.
/// Default: *"Developed with the amazing PageTop framework."*.
pub description: String,
/// Default theme.
/// Default: *""*.
pub theme: String,
/// Default language (localization).
/// Default: *"en-US"*.
pub language: String,
/// Default text direction: *"ltr"* (left-to-right), *"rtl"* (right-to-left), or *"auto"*.
/// Default: *"ltr"*.
pub text_direction: String,
/// ASCII banner printed at startup: *"Off"*, *"Slant"*, *"Small"*, *"Speed"*, or *"Starwars"*.
/// Default: *"Slant"*.
pub startup_banner: String,
/// Default: according to the `PAGETOP_RUN_MODE` environment variable, or *"default"* if unset.
pub run_mode: String,
}
#[derive(Debug, Deserialize)]
/// Section `[dev]` of the configuration settings.
///
/// See [`Settings`].
pub struct Dev {
/// Static files required by the application are integrated by default into the executable
/// binary. However, during development, it can be useful to serve these files from their own
/// directory to avoid recompilation every time they are modified. In this case, specify the
/// full path to the project's root directory.
/// Default: *""*.
pub pagetop_project_dir: String,
}
#[derive(Debug, Deserialize)]
/// Section `[log]` of the configuration settings.
///
/// See [`Settings`].
pub struct Log {
/// Filter, or a comma-separated combination of filters, for execution traces: *"Error"*,
/// *"Warn"*, *"Info"*, *"Debug"*, or *"Trace"*.
/// Example: "Error,actix_server::builder=Info,tracing_actix_web=Debug".
/// Default: *"Info"*.
pub tracing: String,
/// Displays traces in the terminal (*"Stdout"*) or logs them in files with rotation: *"Daily"*,
/// *"Hourly"*, *"Minutely"*, or *"Endless"*.
/// Default: *"Stdout"*.
pub rolling: String,
/// Directory for trace files (if `rolling` != *"Stdout"*).
/// Default: *"log"*.
pub path: String,
/// Prefix for trace files (if `rolling` != *"Stdout"*).
/// Default: *"tracing.log"*.
pub prefix: String,
/// Trace output format. Options are *"Full"*, *"Compact"*, *"Pretty"*, or *"Json"*.
/// Default: *"Full"*.
pub format: String,
}
#[derive(Debug, Deserialize)]
/// Section `[server]` of the configuration settings.
///
/// See [`Settings`].
pub struct Server {
/// Web server bind address.
/// Default: *"localhost"*.
pub bind_address: String,
/// Web server bind port.
/// Default: *8088*.
pub bind_port: u16,
/// Session cookie duration in seconds (0 means "until the browser is closed").
/// Default: *604800* (7 days).
pub session_lifetime: i64,
}

View file

@ -1,4 +0,0 @@
//! HTML in code.
mod maud;
pub use maud::{html, html_private, Markup, PreEscaped, DOCTYPE};

View file

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

View file

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

View file

@ -1,113 +0,0 @@
//! <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>
//!
//! [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?style=for-the-badge)](https://github.com/manuelcillero/pagetop#-license)
//! [![API Docs](https://img.shields.io/docsrs/pagetop?label=API%20Docs&style=for-the-badge&logo=Docs.rs)](https://docs.rs/pagetop)
//! [![Crates.io](https://img.shields.io/crates/v/pagetop.svg?style=for-the-badge&logo=ipfs)](https://crates.io/crates/pagetop)
//! [![Downloads](https://img.shields.io/crates/d/pagetop.svg?style=for-the-badge&logo=transmission)](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, its 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 *************************************************************************************
pub use concat_string::concat_string;
/// Enables flexible identifier concatenation in macros, allowing new items with pasted identifiers.
pub use paste::paste;
pub use pagetop_macros::{html, main, test, AutoDefault};
pub type StaticResources = std::collections::HashMap<&'static str, static_files::Resource>;
pub use std::any::TypeId;
pub type Weight = i8;
// API *********************************************************************************************
// Useful functions and macros.
pub mod util;
// Application tracing and event logging.
pub mod trace;
// HTML in code.
pub mod html;
// 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;
// Global settings.
pub mod global;
// Prepare and run the application.
pub mod app;
// The PageTop Prelude *****************************************************************************
pub mod prelude;

View file

@ -1,281 +0,0 @@
//! Localization (L10n).
//!
//! PageTop uses the [Fluent](https://www.projectfluent.org/) specifications for application
//! localization, leveraging the [fluent-templates](https://docs.rs/fluent-templates/) crate to
//! integrate translation resources directly into the application binary.
//!
//! # 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 both readable and
//! expressive, enabling complex natural language constructs like gender, plurals, and conjugations.
//!
//! # Fluent Resources
//!
//! Localization resources are organized in the *src/locale* directory, with subdirectories for
//! each valid [Unicode Language Identifier](https://docs.rs/unic-langid/):
//!
//! ```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::html::{Markup, PreEscaped};
use crate::{global, kv, AutoDefault};
pub use fluent_bundle::FluentValue;
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";
/// A mapping between language codes (e.g., "en-US") and their corresponding [`LanguageIdentifier`]
/// and human-readable names.
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 DEFAULT_LANGID: LazyLock<&LanguageIdentifier> =
LazyLock::new(|| langid_for(&global::SETTINGS.app.language).unwrap_or(&LANGID_FALLBACK));
pub fn langid_for(language: impl Into<String>) -> Result<&'static LanguageIdentifier, String> {
let language = language.into();
if language.is_empty() {
return Ok(&LANGID_FALLBACK);
}
LANGUAGES
.get(&language)
.map(|(langid, _)| langid)
.ok_or_else(|| format!("No langid for Unicode Language Identifier \"{language}\"."))
}
#[macro_export]
/// Defines a set of localization elements and local translation texts, removing Unicode isolating
/// marks around arguments to improve readability and compatibility in certain rendering contexts.
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),
};
}
};
}
static_locales!(LOCALES_PAGETOP);
#[derive(AutoDefault)]
enum L10nOp {
#[default]
None,
Text(String),
Translate(String),
}
#[derive(AutoDefault)]
pub struct L10n {
op: L10nOp,
locales: Option<&'static Locales>,
args: HashMap<String, FluentValue<'static>>,
}
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(), FluentValue::from(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)
}
}
None => None,
},
}
}
/// Escapes the content using the default language identifier.
pub fn markup(&self) -> Markup {
let content = self.using(&DEFAULT_LANGID).unwrap_or_default();
PreEscaped(content)
}
/// Escapes the content using the specified language identifier.
pub fn escaped(&self, langid: &LanguageIdentifier) -> Markup {
let content = self.using(langid).unwrap_or_default();
PreEscaped(content)
}
}
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,
_ => &DEFAULT_LANGID,
},
key,
)
} else {
locales.lookup_with_args(
match key.as_str() {
LANGUAGE_SET_FAILURE => &LANGID_FALLBACK,
_ => &DEFAULT_LANGID,
},
key,
&self.args,
)
}
)
} else {
write!(f, "Unknown localization {key}")
}
}
}
}
}

View file

@ -1,20 +0,0 @@
welcome_title = Hello world!
welcome_intro = Verifying the proper operation of your { $app } installation.
welcome_powered = A web solution powered by { $pagetop }.
welcome_page = Welcome Page
welcome_subtitle = Are you a { $app } user?
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 issue persists, please contact your system administrator for assistance.
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 designed to create modular, extensible, and configurable web solutions.
welcome_pagetop_text3 = For detailed information, please visit the <a href="https://docs.rs/pagetop/latest/pagetop" target="_blank">official technical documentation</a>.
welcome_issues_title = Reporting Issues
welcome_issues_text1 = To report any issues with PageTop, please use <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank">GitHub</a>. However, check the existing error reports to avoid duplicates.
welcome_issues_text2 = For issues specific to { $app }, please refer to its official repository or support channel, rather than directly to PageTop.
welcome_have_fun = Write code. Break nothing. Repeat.

View file

@ -1,20 +0,0 @@
welcome_title = ¡Hola mundo!
welcome_intro = Verificando el funcionamiento de tu instalación de { $app }.
welcome_powered = Una solución web con la potencia de { $pagetop }.
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, por favor 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>, diseñado para crear soluciones web modulares, extensibles y configurables.
welcome_pagetop_text3 = Para más información visita la <a href="https://docs.rs/pagetop/latest/pagetop" target="_blank">documentación técnica oficial</a>.
welcome_issues_title = Informando Problemas
welcome_issues_text1 = Para comunicar cualquier problema con PageTop utiliza <a href="https://github.com/manuelcillero/pagetop/issues" target="_blank">GitHub</a>. No obstante, comprueba los informes de errores ya existentes para evitar duplicados.
welcome_issues_text2 = Si son fallos específicos de { $app }, por favor acude a su repositorio oficial o canal de soporte, y no al de PageTop directamente.
welcome_have_fun = Escribe código. No rompas nada. Repite.

View file

@ -1,43 +0,0 @@
//! The `PageTop` Prelude.
// RE-EXPORTED.
pub use crate::{concat_string, html, main, paste, test};
pub use crate::{AutoDefault, StaticResources, TypeId, Weight};
// MACROS.
// crate::util
pub use crate::{kv, static_config};
// 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::util;
pub use crate::trace;
pub use crate::html::*;
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::core::theme::*;
pub use crate::response::{json::*, redirect::*, ResponseError};
pub use crate::global;
pub use crate::app::Application;

View file

@ -1,7 +0,0 @@
//! Web request response variants.
pub use actix_web::ResponseError;
pub mod json;
pub mod redirect;

View file

@ -1 +0,0 @@
pub use actix_web::web::Json;

View file

@ -1,76 +0,0 @@
//! 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()
}
}

View file

@ -1,63 +0,0 @@
//! 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<StaticResources> = 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::util::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(),
));
}
});
}
}};
}

View file

@ -1,84 +0,0 @@
//! 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
});

View file

@ -1,298 +0,0 @@
//! Useful functions and macros.
pub mod config;
use crate::trace;
use std::io;
use std::path::PathBuf;
// USEFUL FUNCTIONS ********************************************************************************
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)
}
// USEFUL MACROS ***********************************************************************************
#[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
}};
}
#[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.
///
/// 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 [`static_config!`](crate::static_config) 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,
/// }
///
/// static_config!(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);
/// }
/// ```
macro_rules! static_config {
( $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::util::config::CONFIG_DATA.clone();
$(
settings.set_default($key, $value).unwrap();
)*
match settings.try_into() {
Ok(s) => s,
Err(e) => panic!("Error parsing settings: {}", e),
}
});
};
}

View file

@ -1,59 +0,0 @@
//! Retrieve settings values from configuration files.
mod data;
mod de;
mod error;
mod file;
mod path;
mod source;
mod value;
use crate::concat_string;
use crate::util::config::data::ConfigData;
use crate::util::config::file::File;
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 rm = 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(&concat_string!(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(&concat_string!(config_dir, "/", rm, ".toml")).required(false))
.expect(&format!("Failed to merge {rm}.toml configuration"))
// Add reserved local configuration for the environment. Defaults to 'local.default.toml'.
.merge(File::with_name(&concat_string!(config_dir, "/local.", rm, ".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(&concat_string!(config_dir, "/local.toml")).required(false))
.expect("Failed to merge general reserved local configuration")
// Save the execution mode.
.set("app.run_mode", rm)
.expect("Failed to set application run mode");
settings
});

View file

@ -1,136 +0,0 @@
use crate::util::config::error::*;
use crate::util::config::path;
use crate::util::config::source::Source;
use crate::util::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()
}
}

View file

@ -1,462 +0,0 @@
use crate::util::config::data::ConfigData;
use crate::util::config::error::*;
use crate::util::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
}
}

View file

@ -1,222 +0,0 @@
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())
}
}

View file

@ -1,85 +0,0 @@
mod source;
mod toml;
use crate::util::config::error::*;
use crate::util::config::source::Source;
use crate::util::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 })
}
}

View file

@ -1,126 +0,0 @@
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())
}
}

View file

@ -1,51 +0,0 @@
use crate::util::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()),
}
}

View file

@ -1,167 +0,0 @@
use crate::util::config::error::*;
use crate::util::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;
}
}
}
}
}
}

View file

@ -1,131 +0,0 @@
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);
}
}

View file

@ -1,87 +0,0 @@
use crate::util::config::error::*;
use crate::util::config::path;
use crate::util::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!();
}
}
}

View file

@ -1,545 +0,0 @@
use crate::util::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)
}
}