mirror of
https://github.com/askama-rs/askama.git
synced 2025-09-30 06:21:13 +00:00
Caching reading Config and make it lifetime-free
This commit is contained in:
parent
57bb4a67cb
commit
c67b74ee9f
@ -1,8 +1,9 @@
|
||||
use std::borrow::Cow;
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::collections::btree_map::{BTreeMap, Entry};
|
||||
use std::mem::transmute;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::{env, fs};
|
||||
|
||||
use parser::node::Whitespace;
|
||||
@ -14,20 +15,91 @@ use serde::Deserialize;
|
||||
use crate::{CompileError, FileInfo, CRATE};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Config<'a> {
|
||||
pub(crate) struct Config {
|
||||
pub(crate) dirs: Vec<PathBuf>,
|
||||
pub(crate) syntaxes: BTreeMap<String, SyntaxAndCache<'a>>,
|
||||
pub(crate) default_syntax: &'a str,
|
||||
pub(crate) escapers: Vec<(Vec<Cow<'a, str>>, Cow<'a, str>)>,
|
||||
pub(crate) syntaxes: BTreeMap<String, SyntaxAndCache<'static>>,
|
||||
pub(crate) default_syntax: &'static str,
|
||||
pub(crate) escapers: Vec<(Vec<Cow<'static, str>>, Cow<'static, str>)>,
|
||||
pub(crate) whitespace: WhitespaceHandling,
|
||||
// `Config` is self referencial and `_key` owns it data, so it must come last
|
||||
_key: OwnedConfigKey,
|
||||
}
|
||||
|
||||
impl<'a> Config<'a> {
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
|
||||
struct OwnedConfigKey(Arc<ConfigKey<'static>>);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
struct ConfigKey<'a> {
|
||||
source: Cow<'a, str>,
|
||||
config_path: Option<Cow<'a, str>>,
|
||||
template_whitespace: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> ToOwned for ConfigKey<'a> {
|
||||
type Owned = OwnedConfigKey;
|
||||
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
OwnedConfigKey(Arc::new(ConfigKey {
|
||||
source: Cow::Owned(self.source.as_ref().to_owned()),
|
||||
config_path: self
|
||||
.config_path
|
||||
.as_ref()
|
||||
.map(|s| Cow::Owned(s.as_ref().to_owned())),
|
||||
template_whitespace: self
|
||||
.template_whitespace
|
||||
.as_ref()
|
||||
.map(|s| Cow::Owned(s.as_ref().to_owned())),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Borrow<ConfigKey<'a>> for OwnedConfigKey {
|
||||
fn borrow(&self) -> &ConfigKey<'a> {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub(crate) fn new(
|
||||
s: &'a str,
|
||||
source: &str,
|
||||
config_path: Option<&str>,
|
||||
template_whitespace: Option<&str>,
|
||||
) -> std::result::Result<Config<'a>, CompileError> {
|
||||
) -> Result<&'static Config, CompileError> {
|
||||
static CACHE: OnceLock<Cache<OwnedConfigKey, Arc<Config>>> = OnceLock::new();
|
||||
|
||||
let key = ConfigKey {
|
||||
source: source.into(),
|
||||
config_path: config_path.map(Cow::Borrowed),
|
||||
template_whitespace: template_whitespace.map(Cow::Borrowed),
|
||||
};
|
||||
|
||||
let cache = CACHE.get_or_init(|| Cache::new(8));
|
||||
let config = match cache.get_value_or_guard(&key, None) {
|
||||
GuardResult::Value(config) => config,
|
||||
GuardResult::Guard(guard) => {
|
||||
// we won't be able to use `guard.insert()` because we want to use our own key
|
||||
let config = Config::new_uncached(key.to_owned())?;
|
||||
cache.insert(config._key.clone(), Arc::clone(&config));
|
||||
drop(guard); // `guard` must be dropped after insert
|
||||
config
|
||||
}
|
||||
GuardResult::Timeout => unreachable!("we don't define a timeout"),
|
||||
};
|
||||
|
||||
// SAFETY: an inserted `Config` will never be evicted
|
||||
Ok(unsafe { transmute::<&Config, &'static Config>(config.as_ref()) })
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn new_uncached(key: OwnedConfigKey) -> Result<Arc<Config>, CompileError> {
|
||||
// SAFETY: the resulting `Config` will keep a reference to the `key`
|
||||
let eternal_key =
|
||||
unsafe { transmute::<&ConfigKey<'_>, &'static ConfigKey<'static>>(key.borrow()) };
|
||||
let s = eternal_key.source.as_ref();
|
||||
let config_path = eternal_key.config_path.as_deref();
|
||||
let template_whitespace = eternal_key.template_whitespace.as_deref();
|
||||
|
||||
let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let default_dirs = vec![root.join("templates")];
|
||||
|
||||
@ -110,20 +182,21 @@ impl<'a> Config<'a> {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Config {
|
||||
Ok(Arc::new(Config {
|
||||
dirs,
|
||||
syntaxes,
|
||||
default_syntax,
|
||||
escapers,
|
||||
whitespace,
|
||||
})
|
||||
_key: key,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn find_template(
|
||||
&self,
|
||||
path: &str,
|
||||
start_at: Option<&Path>,
|
||||
) -> std::result::Result<Arc<Path>, CompileError> {
|
||||
) -> Result<Arc<Path>, CompileError> {
|
||||
if let Some(root) = start_at {
|
||||
let relative = root.with_file_name(path);
|
||||
if relative.exists() {
|
||||
@ -271,14 +344,14 @@ struct RawConfig<'a> {
|
||||
|
||||
impl RawConfig<'_> {
|
||||
#[cfg(feature = "config")]
|
||||
fn from_toml_str(s: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
|
||||
fn from_toml_str(s: &str) -> Result<RawConfig<'_>, CompileError> {
|
||||
basic_toml::from_str(s).map_err(|e| {
|
||||
CompileError::no_file_info(format!("invalid TOML in {CONFIG_FILE_NAME}: {e}"))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "config"))]
|
||||
fn from_toml_str(_: &str) -> std::result::Result<RawConfig<'_>, CompileError> {
|
||||
fn from_toml_str(_: &str) -> Result<RawConfig<'_>, CompileError> {
|
||||
Err(CompileError::no_file_info("TOML support not available"))
|
||||
}
|
||||
}
|
||||
@ -334,9 +407,7 @@ struct RawEscaper<'a> {
|
||||
extensions: Vec<&'a str>,
|
||||
}
|
||||
|
||||
pub(crate) fn read_config_file(
|
||||
config_path: Option<&str>,
|
||||
) -> std::result::Result<String, CompileError> {
|
||||
pub(crate) fn read_config_file(config_path: Option<&str>) -> Result<String, CompileError> {
|
||||
let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let filename = match config_path {
|
||||
Some(config_path) => root.join(config_path),
|
||||
@ -357,8 +428,8 @@ pub(crate) fn read_config_file(
|
||||
}
|
||||
}
|
||||
|
||||
fn str_set<'a>(vals: &[&'a str]) -> Vec<Cow<'a, str>> {
|
||||
vals.iter().copied().map(Cow::from).collect()
|
||||
fn str_set(vals: &[&'static str]) -> Vec<Cow<'static, str>> {
|
||||
vals.iter().map(|s| Cow::Borrowed(*s)).collect()
|
||||
}
|
||||
|
||||
static CONFIG_FILE_NAME: &str = "rinja.toml";
|
||||
@ -536,6 +607,14 @@ mod tests {
|
||||
#[cfg(feature = "config")]
|
||||
#[test]
|
||||
fn illegal_delimiters() {
|
||||
#[track_caller]
|
||||
fn expect_err<T, E>(result: Result<T, E>) -> E {
|
||||
match result {
|
||||
Ok(_) => panic!("should have failed"),
|
||||
Err(err) => err,
|
||||
}
|
||||
}
|
||||
|
||||
let raw_config = r#"
|
||||
[[syntax]]
|
||||
name = "too_short"
|
||||
@ -543,7 +622,7 @@ mod tests {
|
||||
"#;
|
||||
let config = Config::new(raw_config, None, None);
|
||||
assert_eq!(
|
||||
config.unwrap_err().msg,
|
||||
expect_err(config).msg,
|
||||
r#"delimiters must be at least two characters long: "<""#,
|
||||
);
|
||||
|
||||
@ -554,7 +633,7 @@ mod tests {
|
||||
"#;
|
||||
let config = Config::new(raw_config, None, None);
|
||||
assert_eq!(
|
||||
config.unwrap_err().msg,
|
||||
expect_err(config).msg,
|
||||
r#"delimiters may not contain white spaces: " {{ ""#,
|
||||
);
|
||||
|
||||
@ -567,7 +646,7 @@ mod tests {
|
||||
"#;
|
||||
let config = Config::new(raw_config, None, None);
|
||||
assert_eq!(
|
||||
config.unwrap_err().msg,
|
||||
expect_err(config).msg,
|
||||
r#"a delimiter may not be the prefix of another delimiter: "{{" vs "{{$""#,
|
||||
);
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ impl Context<'_> {
|
||||
}
|
||||
|
||||
pub(crate) fn new<'n>(
|
||||
config: &Config<'_>,
|
||||
config: &Config,
|
||||
path: &'n Path,
|
||||
parsed: &'n Parsed,
|
||||
) -> Result<Context<'n>, CompileError> {
|
||||
|
@ -16,7 +16,7 @@ use crate::{CompileError, FileInfo, MsgValidEscapers};
|
||||
|
||||
pub(crate) struct TemplateInput<'a> {
|
||||
pub(crate) ast: &'a syn::DeriveInput,
|
||||
pub(crate) config: &'a Config<'a>,
|
||||
pub(crate) config: &'a Config,
|
||||
pub(crate) syntax: &'a SyntaxAndCache<'a>,
|
||||
pub(crate) source: &'a Source,
|
||||
pub(crate) block: Option<&'a str>,
|
||||
@ -33,7 +33,7 @@ impl TemplateInput<'_> {
|
||||
/// `template()` attribute list fields.
|
||||
pub(crate) fn new<'n>(
|
||||
ast: &'n syn::DeriveInput,
|
||||
config: &'n Config<'_>,
|
||||
config: &'n Config,
|
||||
args: &'n TemplateArgs,
|
||||
) -> Result<TemplateInput<'n>, CompileError> {
|
||||
let TemplateArgs {
|
||||
|
@ -105,7 +105,7 @@ pub fn derive_template(input: TokenStream12) -> TokenStream12 {
|
||||
fn build_skeleton(ast: &syn::DeriveInput) -> Result<String, CompileError> {
|
||||
let template_args = TemplateArgs::fallback();
|
||||
let config = Config::new("", None, None)?;
|
||||
let input = TemplateInput::new(ast, &config, &template_args)?;
|
||||
let input = TemplateInput::new(ast, config, &template_args)?;
|
||||
let mut contexts = HashMap::new();
|
||||
let parsed = parser::Parsed::default();
|
||||
contexts.insert(&input.path, Context::empty(&parsed));
|
||||
@ -131,7 +131,7 @@ pub(crate) fn build_template(ast: &syn::DeriveInput) -> Result<String, CompileEr
|
||||
let config_path = template_args.config_path();
|
||||
let s = read_config_file(config_path)?;
|
||||
let config = Config::new(&s, config_path, template_args.whitespace.as_deref())?;
|
||||
let input = TemplateInput::new(ast, &config, &template_args)?;
|
||||
let input = TemplateInput::new(ast, config, &template_args)?;
|
||||
|
||||
let mut templates = HashMap::new();
|
||||
input.find_used_templates(&mut templates)?;
|
||||
|
Loading…
x
Reference in New Issue
Block a user