1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
//! Configuration loader trait and implementations.
//!
//! A configuration loader only loads contents of one or more plugins. No parsing is done here.
//! The result is just a hashmap with plugin names (in lowercase) as keys and [ConfigurationEntity]
//! as values.
//! A loader also should try to set contents format for each plugin.
//! For example [fs] (that implements [ConfigurationLoader]) that loads
//! configurations from filesystem, guesses content formats from file extensions.
//!
//! Main method of [ConfigurationLoader] trait is `try_load` that accepts a URL and maybe a
//! whitelist of plugin names. It can parse the URL to detect and validate its own options.
//! For example [mod@env] that loads configuration from environment-variables
//! accepts a URL like `env://?prefix=MY_APP_NAME` and [fs] accepts a URL
//! like `file:///path/to/a/file.json?skippable[0]=notfound` (`skippable` is a list and should
//! contain error kinds that we want to skip if they happen).
//!
//! Note that generally you do not need to implement [ConfigurationLoader] for your own structs and
//! provided [closure] lets you implement your own loader with just one [Fn] closure.
use crate::entity::ConfigurationEntity;
use serde::de::DeserializeOwned;
use std::{collections::HashMap, fmt::Debug};
use url::Url;
pub mod closure;
#[cfg(feature = "env")]
pub mod env;
#[cfg(feature = "fs")]
pub mod fs;
/// A modifier [Fn] that modifies loaded configurations (if needed).
pub type BoxedLoaderModifierFn = Box<
dyn Fn(&Url, &mut HashMap<String, ConfigurationEntity>) -> Result<(), ConfigurationLoadError>
+ Send
+ Sync,
>;
/// Loaded error type.
#[derive(Debug, thiserror::Error)]
pub enum ConfigurationLoadError {
/// An entity could not be found.
#[error("{loader} configuration loader could not found configuration `{url}`")]
NotFound { loader: String, url: Url },
/// Did not have enough permissions to read the contents.
#[error("{loader} configuration loader has no access to load configuration from `{url}`")]
NoAccess { loader: String, url: Url },
/// Got timeout when reading the contents.
#[error(
"{loader} configuration loader reached timeout `{timeout_in_seconds}s` to load `{url}`"
)]
Timeout {
loader: String,
url: Url,
timeout_in_seconds: usize,
},
/// The provided URL is invalid.
#[error("{loader} configuration loader got invalid URL `{url}`")]
InvalidUrl {
loader: String,
url: String,
source: anyhow::Error,
},
/// Could not found URL scheme.
#[error("Could not found configuration loader for scheme {scheme}")]
UrlSchemeNotFound { scheme: String },
/// Found more than one configuration with two different formats (extensions) for the same plugin.
#[error("{loader} configuration loader found duplicate configurations `{url}/{plugin}.({format_1}|{format_2})`")]
Duplicate {
loader: String,
url: Url,
plugin: String,
format_1: String,
format_2: String,
},
/// Could not load the configuration.
///
/// note that `skippable` key is very important. You might want to detect your own options from
/// provided [Url] and sometimes make some errors skippable based on you detected options.
#[error("{loader} configuration loader could not {description} `{url}`")]
Load {
loader: String,
url: Url,
description: String,
source: anyhow::Error,
skippable: bool,
},
#[error("Could not acquire lock for configuration loader with url `{url}`")]
AcquireLock { url: Url },
#[error(transparent)]
Other(#[from] anyhow::Error),
}
/// A trait to load configurations for one or more plugins.
pub trait ConfigurationLoader: Send + Sync + Debug {
/// Name of the loader (for logging purposes).
fn name(&self) -> &'static str;
/// List of URL schemes that this loader supports.
///
/// Different URL may be assigned to this loader by their scheme value.
fn scheme_list(&self) -> Vec<String>;
/// Main method that actually loads configurations.
///
/// * Checks the `url` and detects its own options from it.
/// * Checks whitelist to load just provided plugins configurations.
/// * Attempts to load configurations.
/// * Tries to set format for each [ConfigurationEntity].
fn try_load(
&self,
url: &Url,
maybe_whitelist: Option<&[String]>,
) -> Result<HashMap<String, ConfigurationEntity>, ConfigurationLoadError>;
}
#[cfg(feature = "qs")]
/// Checks query-string part of URL and tries to deserialize it to provided type.
///
/// See supported syntax at [serde_qs].
/// This function is only usable if `qs` Cargo feature is enabled.
pub fn deserialize_query_string<T: DeserializeOwned>(
loader_name: &'static str,
url: &Url,
) -> Result<T, ConfigurationLoadError> {
serde_qs::from_str(url.query().unwrap_or_default()).map_err(|error| {
ConfigurationLoadError::InvalidUrl {
loader: loader_name.to_string(),
source: error.into(),
url: url.to_string(),
}
})
}
impl ConfigurationLoadError {
pub fn is_skippable(&self) -> bool {
if let Self::Load { skippable, .. } = self {
*skippable
} else {
false
}
}
}