From a27d5aebf6afe3e49aadb53244f3d0c30095b5a4 Mon Sep 17 00:00:00 2001 From: Jiahao XU Date: Sun, 26 Mar 2023 16:11:10 +1100 Subject: [PATCH] Use leon for template in binstalk & detect malformed pkg-url/pkg-fmt early (#933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed #851 * Add new dep leon to crate binstalk * Add new variant `BinstallError::Template{Parse, Render}Error` * Use `leon::Template` in mod `bins` * Use `leon::Template` in mod `fetchers::gh_crate_meta` * Refactor mod `bins`: Rm unused associated fn & fields from `Context` * Simplify unit testing in mod `fetchers::gh_crate_meta` * Rm soft-deprecated field `fetchers::gh_crate_meta::Context::format` and change the `match` to resolve `archive` => `self.archive_format`. * Make macro_rules `leon::template!` far easier to use * Construct `leon::Template<'_>` as constant in `gh_crate_meta::hosting` * Simplify `leon::Values` trait Change its method `get_value` signature to ```rust fn get_value(&self, key: &str) -> Option>; ``` Now, `ValuesFn` also accepts non-`'static` function, but now `leon::Values` is only implemented for `&ValuesFn` now. This makes it a little bit more cumbersome to use but I think it's a reasonable drawback. * Rm `Send` bound req from `ValuesFn` * Impl new fn `leon::Template::cast` for casting `Template<'s>` to `Template<'t>` where `'s: 't` * Rename `leon::Template::has_keys` => `has_any_of_keys` * Make checking whether format related keys are present more robust * Optimize `GhCrateMeta::launch_baseline_find_tasks`: Skip checking all fmt ext if none of the format related keys ("format", "archive-format", "archive-suffix") are present. * Only ret `.exe` in `PkgFmt::extensions` on windows by adding a new param `is_windows: bool` * Improve debug msg in `GhCrateMeta::fetch_and_extract` * Add warnings to `GhCrateMeta::find` * Rm dep tinytemplate * `impl<'s, 'rhs: 's> ops::AddAssign<&Template<'rhs>> for Template<'s>` * `impl<'s, 'rhs: 's> ops::AddAssign> for Template<'s>` * `impl<'s, 'item: 's> ops::AddAssign> for Template<'s>` * `impl<'s, 'item: 's> ops::AddAssign<&Item<'item>> for Template<'s>` * `impl<'s, 'rhs: 's> ops::Add> for Template<'s>` (improved existing `Add` impl) * `impl<'s, 'rhs: 's> ops::Add<&Template<'rhs>> for Template<'s>` * `impl<'s, 'item: 's> ops::Add> for Template<'s>` * `impl<'s, 'item: 's> ops::Add<&Item<'item>> for Template<'s>` Signed-off-by: Jiahao XU Co-authored-by: Félix Saparelli --- Cargo.lock | 2 +- .../cargo_toml_binstall/package_formats.rs | 13 +- crates/binstalk/Cargo.toml | 2 +- crates/binstalk/src/bins.rs | 31 +-- crates/binstalk/src/errors.rs | 34 ++- crates/binstalk/src/fetchers/gh_crate_meta.rs | 219 ++++++++++-------- .../src/fetchers/gh_crate_meta/hosting.rs | 99 +++++--- crates/binstalk/src/ops/resolve.rs | 8 +- crates/leon/src/lib.rs | 6 +- crates/leon/src/macros.rs | 136 +++++++++-- crates/leon/src/parser.rs | 123 +++------- crates/leon/src/template.rs | 114 +++++++-- crates/leon/src/values.rs | 37 ++- 13 files changed, 512 insertions(+), 312 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f31eeb2..49ac3c13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,7 @@ dependencies = [ "home", "itertools", "jobslot", + "leon", "maybe-owned", "miette", "normalize-path", @@ -178,7 +179,6 @@ dependencies = [ "strum", "tempfile", "thiserror", - "tinytemplate", "tokio", "tracing", "url", diff --git a/crates/binstalk-types/src/cargo_toml_binstall/package_formats.rs b/crates/binstalk-types/src/cargo_toml_binstall/package_formats.rs index 46a82ca1..8619bddc 100644 --- a/crates/binstalk-types/src/cargo_toml_binstall/package_formats.rs +++ b/crates/binstalk-types/src/cargo_toml_binstall/package_formats.rs @@ -46,14 +46,23 @@ impl PkgFmt { /// List of possible file extensions for the format /// (with prefix `.`). - pub fn extensions(self) -> &'static [&'static str] { + /// + /// * `is_windows` - if true and `self == PkgFmt::Bin`, then it will return + /// `.exe` in additional to other bin extension names. + pub fn extensions(self, is_windows: bool) -> &'static [&'static str] { match self { PkgFmt::Tar => &[".tar"], PkgFmt::Tbz2 => &[".tbz2", ".tar.bz2"], PkgFmt::Tgz => &[".tgz", ".tar.gz"], PkgFmt::Txz => &[".txz", ".tar.xz"], PkgFmt::Tzstd => &[".tzstd", ".tzst", ".tar.zst"], - PkgFmt::Bin => &[".bin", ".exe", ""], + PkgFmt::Bin => { + if is_windows { + &[".bin", "", ".exe"] + } else { + &[".bin", ""] + } + } PkgFmt::Zip => &[".zip"], } } diff --git a/crates/binstalk/Cargo.toml b/crates/binstalk/Cargo.toml index 99e5d4e6..a5533a97 100644 --- a/crates/binstalk/Cargo.toml +++ b/crates/binstalk/Cargo.toml @@ -21,6 +21,7 @@ either = "1.8.1" home = "0.5.4" itertools = "0.10.5" jobslot = { version = "0.2.10", features = ["tokio"] } +leon = { version = "1.0.0", path = "../leon" } maybe-owned = "0.3.4" miette = "5.6.0" normalize-path = { version = "0.2.0", path = "../normalize-path" } @@ -30,7 +31,6 @@ serde = { version = "1.0.157", features = ["derive"] } strum = "0.24.1" tempfile = "3.4.0" thiserror = "1.0.40" -tinytemplate = "1.2.1" # parking_lot for `tokio::sync::OnceCell::const_new` tokio = { version = "1.26.0", features = ["rt", "process", "sync", "signal", "parking_lot"], default-features = false } tracing = "0.1.37" diff --git a/crates/binstalk/src/bins.rs b/crates/binstalk/src/bins.rs index 2e216b55..0b95ef76 100644 --- a/crates/binstalk/src/bins.rs +++ b/crates/binstalk/src/bins.rs @@ -5,9 +5,8 @@ use std::{ }; use compact_str::{format_compact, CompactString}; +use leon::Template; use normalize_path::NormalizePath; -use serde::Serialize; -use tinytemplate::TinyTemplate; use tracing::debug; use crate::{ @@ -80,7 +79,7 @@ impl BinFile { pub fn new( data: &Data<'_>, base_name: &str, - tt: &TinyTemplate, + tt: &Template<'_>, no_symlinks: bool, ) -> Result { let binary_ext = if data.target.contains("windows") { @@ -95,7 +94,6 @@ impl BinFile { target: data.target, version: data.version, bin: base_name, - format: binary_ext, binary_ext, }; @@ -107,7 +105,7 @@ impl BinFile { } else { // Generate install paths // Source path is the download dir + the generated binary path - let path = ctx.render_with_compiled_tt(tt)?; + let path = tt.render(&ctx)?; let path_normalized = Path::new(&path).normalize(); @@ -238,7 +236,7 @@ pub struct Data<'a> { pub install_path: &'a Path, } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug)] struct Context<'c> { pub name: &'c str, pub repo: Option<&'c str>, @@ -246,18 +244,23 @@ struct Context<'c> { pub version: &'c str, pub bin: &'c str, - /// Soft-deprecated alias for binary-ext - pub format: &'c str, - /// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise - #[serde(rename = "binary-ext")] pub binary_ext: &'c str, } -impl<'c> Context<'c> { - /// * `tt` - must have a template with name "bin_dir" - fn render_with_compiled_tt(&self, tt: &TinyTemplate) -> Result { - Ok(tt.render("bin_dir", self)?) +impl leon::Values for Context<'_> { + fn get_value<'s>(&'s self, key: &str) -> Option> { + match key { + "name" => Some(Cow::Borrowed(self.name)), + "repo" => self.repo.map(Cow::Borrowed), + "target" => Some(Cow::Borrowed(self.target)), + "version" => Some(Cow::Borrowed(self.version)), + "bin" => Some(Cow::Borrowed(self.bin)), + "binary-ext" => Some(Cow::Borrowed(self.binary_ext)), + // Soft-deprecated alias for binary-ext + "format" => Some(Cow::Borrowed(self.binary_ext)), + _ => None, + } } } diff --git a/crates/binstalk/src/errors.rs b/crates/binstalk/src/errors.rs index edd222b4..8fb1f3dd 100644 --- a/crates/binstalk/src/errors.rs +++ b/crates/binstalk/src/errors.rs @@ -11,7 +11,6 @@ use cargo_toml::Error as CargoTomlError; use compact_str::CompactString; use miette::{Diagnostic, Report}; use thiserror::Error; -use tinytemplate::error::Error as TinyTemplateError; use tokio::task; use tracing::{error, warn}; @@ -82,13 +81,33 @@ pub enum BinstallError { #[diagnostic(severity(error), code(binstall::url_parse))] UrlParse(#[from] url::ParseError), - /// A rendering error in a template. + /// Failed to parse template. /// /// - Code: `binstall::template` /// - Exit: 67 + #[error(transparent)] + #[diagnostic(severity(error), code(binstall::template))] + #[source_code(transparent)] + #[label(transparent)] + TemplateParseError( + #[from] + #[diagnostic_source] + leon::ParseError, + ), + + /// Failed to render template. + /// + /// - Code: `binstall::template` + /// - Exit: 69 #[error("Failed to render template: {0}")] #[diagnostic(severity(error), code(binstall::template))] - Template(Box), + #[source_code(transparent)] + #[label(transparent)] + TemplateRenderError( + #[from] + #[diagnostic_source] + leon::RenderError, + ), /// Failed to download or failed to decode the body. /// @@ -308,7 +327,8 @@ impl BinstallError { TaskJoinError(_) => 17, UserAbort => 32, UrlParse(_) => 65, - Template(_) => 67, + TemplateParseError(..) => 67, + TemplateRenderError(..) => 69, Download(_) => 68, SubProcess { .. } => 70, Io(_) => 74, @@ -404,12 +424,6 @@ impl From for BinstallError { } } -impl From for BinstallError { - fn from(e: TinyTemplateError) -> Self { - BinstallError::Template(Box::new(e)) - } -} - impl From for BinstallError { fn from(e: CargoTomlError) -> Self { BinstallError::CargoManifest(Box::new(e)) diff --git a/crates/binstalk/src/fetchers/gh_crate_meta.rs b/crates/binstalk/src/fetchers/gh_crate_meta.rs index 8f93cea4..1fb193a5 100644 --- a/crates/binstalk/src/fetchers/gh_crate_meta.rs +++ b/crates/binstalk/src/fetchers/gh_crate_meta.rs @@ -1,12 +1,10 @@ -use std::{borrow::Cow, future::Future, iter, path::Path, sync::Arc}; +use std::{borrow::Cow, iter, path::Path, sync::Arc}; use compact_str::{CompactString, ToCompactString}; use either::Either; -use itertools::Itertools; +use leon::Template; use once_cell::sync::OnceCell; -use serde::Serialize; use strum::IntoEnumIterator; -use tinytemplate::TinyTemplate; use tracing::{debug, warn}; use url::Url; @@ -35,36 +33,41 @@ pub struct GhCrateMeta { resolution: OnceCell<(Url, PkgFmt)>, } -type FindTaskRes = Result, BinstallError>; - impl GhCrateMeta { - /// * `tt` - must have added a template named "pkg_url". - fn launch_baseline_find_tasks<'a>( - &'a self, + fn launch_baseline_find_tasks( + &self, + futures_resolver: &FuturesResolver<(Url, PkgFmt), BinstallError>, pkg_fmt: PkgFmt, - tt: &'a TinyTemplate, - pkg_url: &'a str, - repo: Option<&'a str>, - ) -> impl Iterator + 'static> + 'a { - // build up list of potential URLs - let urls = pkg_fmt - .extensions() - .iter() - .filter_map(move |ext| { - let ctx = - Context::from_data_with_repo(&self.data, &self.target_data.target, ext, repo); - match ctx.render_url_with_compiled_tt(tt, pkg_url) { - Ok(url) => Some(url), - Err(err) => { - warn!("Failed to render url for {ctx:#?}: {err}"); - None - } + pkg_url: &Template<'_>, + repo: Option<&str>, + ) { + let render_url = |ext| { + let ctx = Context::from_data_with_repo(&self.data, &self.target_data.target, ext, repo); + match ctx.render_url_with_compiled_tt(pkg_url) { + Ok(url) => Some(url), + Err(err) => { + warn!("Failed to render url for {ctx:#?}: {err}"); + None } - }) - .dedup(); + } + }; + + let is_windows = self.target_data.target.contains("windows"); + + let urls = if pkg_url.has_any_of_keys(&["format", "archive-format", "archive-suffix"]) { + // build up list of potential URLs + Either::Left( + pkg_fmt + .extensions(is_windows) + .iter() + .filter_map(|ext| render_url(Some(ext))), + ) + } else { + Either::Right(render_url(None).into_iter()) + }; // go check all potential URLs at once - urls.map(move |url| { + futures_resolver.extend(urls.map(move |url| { let client = self.client.clone(); let gh_api_client = self.gh_api_client.clone(); @@ -73,7 +76,7 @@ impl GhCrateMeta { .await? .then_some((url, pkg_fmt))) } - }) + })); } } @@ -101,10 +104,10 @@ impl super::Fetcher for GhCrateMeta { let mut pkg_fmt = self.target_data.meta.pkg_fmt; let pkg_urls = if let Some(pkg_url) = self.target_data.meta.pkg_url.as_deref() { + let template = Template::parse(pkg_url)?; + if pkg_fmt.is_none() - && !(pkg_url.contains("format") - || pkg_url.contains("archive-format") - || pkg_url.contains("archive-suffix")) + && !template.has_any_of_keys(&["format", "archive-format", "archive-suffix"]) { // The crate does not specify the pkg-fmt, yet its pkg-url // template doesn't contains format, archive-format or @@ -115,23 +118,36 @@ impl super::Fetcher for GhCrateMeta { // just a best-effort pkg_fmt = PkgFmt::guess_pkg_format(pkg_url); + let crate_name = &self.data.name; + let version = &self.data.version; + let target = &self.target_data.target; + if pkg_fmt.is_none() { return Err(InvalidPkgFmtError { - crate_name: self.data.name.clone(), - version: self.data.version.clone(), - target: self.target_data.target.clone(), + crate_name: crate_name.clone(), + version: version.clone(), + target: target.clone(), pkg_url: pkg_url.to_string(), reason: "pkg-fmt is not specified, yet pkg-url does not contain format, archive-format or archive-suffix which is required for automatically deducing pkg-fmt", } .into()); } + + warn!( + "Crate {crate_name}@{version} on target {target} does not specify pkg-fmt \ + but its pkg-url also does not contain key format, archive-format or \ + archive-suffix.\nbinstall was able to guess that from pkg-url, but \ + just note that it could be wrong:\npkg-fmt=\"{pkg_fmt}\", pkg-url=\"{pkg_url}\"", + pkg_fmt = pkg_fmt.unwrap(), + ); } - Either::Left(iter::once(Cow::Borrowed(pkg_url))) + + Either::Left(iter::once(template)) } else if let Some(repo) = repo.as_ref() { if let Some(pkg_urls) = RepositoryHost::guess_git_hosting_services(repo)?.get_default_pkg_url_template() { - Either::Right(pkg_urls.map(Cow::Owned)) + Either::Right(pkg_urls.map(Template::cast)) } else { warn!( concat!( @@ -172,16 +188,12 @@ impl super::Fetcher for GhCrateMeta { // Iterate over pkg_urls first to avoid String::clone. for pkg_url in pkg_urls { - let mut tt = TinyTemplate::new(); - - tt.add_template("pkg_url", &pkg_url)?; - // Clone iter pkg_fmts to ensure all pkg_fmts is // iterated over for each pkg_url, which is // basically cartesian product. // | for pkg_fmt in pkg_fmts.clone() { - resolver.extend(this.launch_baseline_find_tasks(pkg_fmt, &tt, &pkg_url, repo)); + this.launch_baseline_find_tasks(&resolver, pkg_fmt, &pkg_url, repo); } } @@ -197,7 +209,10 @@ impl super::Fetcher for GhCrateMeta { async fn fetch_and_extract(&self, dst: &Path) -> Result { let (url, pkg_fmt) = self.resolution.get().unwrap(); // find() is called first - debug!("Downloading package from: '{url}' dst:{dst:?} fmt:{pkg_fmt:?}"); + debug!( + "Downloading package from: '{url}' dst:{} fmt:{pkg_fmt:?}", + dst.display() + ); Ok(Download::new(self.client.clone(), url.clone()) .and_extract(*pkg_fmt, dst) .await?) @@ -242,50 +257,67 @@ impl super::Fetcher for GhCrateMeta { } /// Template for constructing download paths -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug)] struct Context<'c> { pub name: &'c str, pub repo: Option<&'c str>, pub target: &'c str, pub version: &'c str, - /// Soft-deprecated alias for archive-format - pub format: &'c str, - /// Archive format e.g. tar.gz, zip - #[serde(rename = "archive-format")] - pub archive_format: &'c str, + pub archive_format: Option<&'c str>, - #[serde(rename = "archive-suffix")] - pub archive_suffix: &'c str, + pub archive_suffix: Option<&'c str>, /// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise - #[serde(rename = "binary-ext")] pub binary_ext: &'c str, } +impl leon::Values for Context<'_> { + fn get_value<'s>(&'s self, key: &str) -> Option> { + match key { + "name" => Some(Cow::Borrowed(self.name)), + "repo" => self.repo.map(Cow::Borrowed), + "target" => Some(Cow::Borrowed(self.target)), + "version" => Some(Cow::Borrowed(self.version)), + + "archive-format" => self.archive_format.map(Cow::Borrowed), + + // Soft-deprecated alias for archive-format + "format" => self.archive_format.map(Cow::Borrowed), + + "archive-suffix" => self.archive_suffix.map(Cow::Borrowed), + + "binary-ext" => Some(Cow::Borrowed(self.binary_ext)), + + _ => None, + } + } +} + impl<'c> Context<'c> { pub(self) fn from_data_with_repo( data: &'c Data, target: &'c str, - archive_suffix: &'c str, + archive_suffix: Option<&'c str>, repo: Option<&'c str>, ) -> Self { - let archive_format = if archive_suffix.is_empty() { - // Empty archive_suffix means PkgFmt::Bin - "bin" - } else { - debug_assert!(archive_suffix.starts_with('.'), "{archive_suffix}"); + let archive_format = archive_suffix.map(|archive_suffix| { + if archive_suffix.is_empty() { + // Empty archive_suffix means PkgFmt::Bin + "bin" + } else { + debug_assert!(archive_suffix.starts_with('.'), "{archive_suffix}"); - &archive_suffix[1..] - }; + &archive_suffix[1..] + } + }); Self { name: &data.name, repo, target, version: &data.version, - format: archive_format, archive_format, archive_suffix, binary_ext: if target.contains("windows") { @@ -298,31 +330,31 @@ impl<'c> Context<'c> { #[cfg(test)] pub(self) fn from_data(data: &'c Data, target: &'c str, archive_format: &'c str) -> Self { - Self::from_data_with_repo(data, target, archive_format, data.repo.as_deref()) + Self::from_data_with_repo(data, target, Some(archive_format), data.repo.as_deref()) } /// * `tt` - must have added a template named "pkg_url". pub(self) fn render_url_with_compiled_tt( &self, - tt: &TinyTemplate, - template: &str, + tt: &Template<'_>, ) -> Result { - debug!("Render {template} using context: {self:?}"); + debug!("Render {tt:#?} using context: {self:?}"); - Ok(Url::parse(&tt.render("pkg_url", self)?)?) + Ok(Url::parse(&tt.render(self)?)?) } #[cfg(test)] pub(self) fn render_url(&self, template: &str) -> Result { - let mut tt = TinyTemplate::new(); - tt.add_template("pkg_url", template)?; - self.render_url_with_compiled_tt(&tt, template) + debug!("Render {template} using context in render_url: {self:?}"); + + let tt = Template::parse(template)?; + self.render_url_with_compiled_tt(&tt) } } #[cfg(test)] mod test { - use crate::manifests::cargo_toml_binstall::{PkgFmt, PkgMeta}; + use crate::manifests::cargo_toml_binstall::PkgMeta; use super::{super::Data, Context}; use compact_str::ToCompactString; @@ -365,10 +397,7 @@ mod test { #[test] fn no_repo_but_full_url() { - let meta = PkgMeta { - pkg_url: Some(format!("https://example.com{DEFAULT_PKG_URL}")), - ..Default::default() - }; + let pkg_url = &format!("https://example.com{}", &DEFAULT_PKG_URL[8..]); let data = Data::new( "cargo-binstall".to_compact_string(), @@ -378,19 +407,15 @@ mod test { let ctx = Context::from_data(&data, "x86_64-unknown-linux-gnu", ".tgz"); assert_eq!( - ctx.render_url(meta.pkg_url.as_deref().unwrap()).unwrap(), + ctx.render_url(pkg_url).unwrap(), url("https://example.com/releases/download/v1.2.3/cargo-binstall-x86_64-unknown-linux-gnu-v1.2.3.tgz") ); } #[test] fn different_url() { - let meta = PkgMeta { - pkg_url: Some( - "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ archive-format }" - .to_string()), - ..Default::default() - }; + let pkg_url = + "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ archive-format }"; let data = Data::new( "radio-sx128x".to_compact_string(), @@ -400,17 +425,14 @@ mod test { let ctx = Context::from_data(&data, "x86_64-unknown-linux-gnu", ".tgz"); assert_eq!( - ctx.render_url(meta.pkg_url.as_deref().unwrap()).unwrap(), + ctx.render_url(pkg_url).unwrap(), url("https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/sx128x-util-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz") ); } #[test] fn deprecated_format() { - let meta = PkgMeta { - pkg_url: Some("{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ format }".to_string()), - ..Default::default() - }; + let pkg_url = "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ format }"; let data = Data::new( "radio-sx128x".to_compact_string(), @@ -420,21 +442,15 @@ mod test { let ctx = Context::from_data(&data, "x86_64-unknown-linux-gnu", ".tgz"); assert_eq!( - ctx.render_url(meta.pkg_url.as_deref().unwrap()).unwrap(), + ctx.render_url(pkg_url).unwrap(), url("https://github.com/rust-iot/rust-radio-sx128x/releases/download/v0.14.1-alpha.5/sx128x-util-x86_64-unknown-linux-gnu-v0.14.1-alpha.5.tgz") ); } #[test] fn different_ext() { - let meta = PkgMeta { - pkg_url: Some( - "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }.tar.xz" - .to_string(), - ), - pkg_fmt: Some(PkgFmt::Txz), - ..Default::default() - }; + let pkg_url = + "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }.tar.xz"; let data = Data::new( "cargo-watch".to_compact_string(), @@ -444,18 +460,15 @@ mod test { let ctx = Context::from_data(&data, "aarch64-apple-darwin", ".txz"); assert_eq!( - ctx.render_url(meta.pkg_url.as_deref().unwrap()).unwrap(), + ctx.render_url(pkg_url).unwrap(), url("https://github.com/watchexec/cargo-watch/releases/download/v9.0.0/cargo-watch-v9.0.0-aarch64-apple-darwin.tar.xz") ); } #[test] fn no_archive() { - let meta = PkgMeta { - pkg_url: Some("{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }{ binary-ext }".to_string()), - pkg_fmt: Some(PkgFmt::Bin), - ..Default::default() - }; + let pkg_url = "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }{ binary-ext }" + ; let data = Data::new( "cargo-watch".to_compact_string(), @@ -465,7 +478,7 @@ mod test { let ctx = Context::from_data(&data, "aarch64-pc-windows-msvc", ".bin"); assert_eq!( - ctx.render_url(meta.pkg_url.as_deref().unwrap()).unwrap(), + ctx.render_url(pkg_url).unwrap(), url("https://github.com/watchexec/cargo-watch/releases/download/v9.0.0/cargo-watch-v9.0.0-aarch64-pc-windows-msvc.exe") ); } diff --git a/crates/binstalk/src/fetchers/gh_crate_meta/hosting.rs b/crates/binstalk/src/fetchers/gh_crate_meta/hosting.rs index 34e4c74e..e6a7ed91 100644 --- a/crates/binstalk/src/fetchers/gh_crate_meta/hosting.rs +++ b/crates/binstalk/src/fetchers/gh_crate_meta/hosting.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use leon::{template, Item, Template}; use url::Url; use crate::errors::BinstallError; @@ -14,20 +15,63 @@ pub enum RepositoryHost { /// Make sure to update possible_dirs in `bins::infer_bin_dir_template` /// if you modified FULL_FILENAMES or NOVERSION_FILENAMES. -pub const FULL_FILENAMES: &[&str] = &[ - "{ name }-{ target }-v{ version }{ archive-suffix }", - "{ name }-{ target }-{ version }{ archive-suffix }", - "{ name }-{ version }-{ target }{ archive-suffix }", - "{ name }-v{ version }-{ target }{ archive-suffix }", - "{ name }_{ target }_v{ version }{ archive-suffix }", - "{ name }_{ target }_{ version }{ archive-suffix }", - "{ name }_{ version }_{ target }{ archive-suffix }", - "{ name }_v{ version }_{ target }{ archive-suffix }", +pub const FULL_FILENAMES: &[Template<'_>] = &[ + template!("/", { "name" }, "-", { "target" }, "-v", { "version" }, { + "archive-suffix" + }), + template!("/", { "name" }, "-", { "target" }, "-", { "version" }, { + "archive-suffix" + }), + template!("/", { "name" }, "-", { "version" }, "-", { "target" }, { + "archive-suffix" + }), + template!("/", { "name" }, "-v", { "version" }, "-", { "target" }, { + "archive-suffix" + }), + template!("/", { "name" }, "_", { "target" }, "_v", { "version" }, { + "archive-suffix" + }), + template!("/", { "name" }, "_", { "target" }, "_", { "version" }, { + "archive-suffix" + }), + template!("/", { "name" }, "_", { "version" }, "_", { "target" }, { + "archive-suffix" + }), + template!("/", { "name" }, "_v", { "version" }, "_", { "target" }, { + "archive-suffix" + }), ]; -pub const NOVERSION_FILENAMES: &[&str] = &[ - "{ name }-{ target }{ archive-suffix }", - "{ name }_{ target }{ archive-suffix }", +pub const NOVERSION_FILENAMES: &[Template<'_>] = &[ + template!("/", { "name" }, "-", { "target" }, { "archive-suffix" }), + template!("/", { "name" }, "_", { "target" }, { "archive-suffix" }), +]; + +const GITHUB_RELEASE_PATHS: &[Template<'_>] = &[ + template!({ "repo" }, "/releases/download/", { "version" }), + template!({ "repo" }, "/releases/download/v", { "version" }), +]; + +const GITLAB_RELEASE_PATHS: &[Template<'_>] = &[ + template!( + { "repo" }, + "/-/releases/", + { "version" }, + "/downloads/binaries" + ), + template!( + { "repo" }, + "/-/releases/v", + { "version" }, + "/downloads/binaries" + ), +]; + +const BITBUCKET_RELEASE_PATHS: &[Template<'_>] = &[template!({ "repo" }, "/downloads")]; + +const SOURCEFORGE_RELEASE_PATHS: &[Template<'_>] = &[ + template!({ "repo" }, "/files/binaries/", { "version" }), + template!({ "repo" }, "/files/binaries/v", { "version" }), ]; impl RepositoryHost { @@ -45,36 +89,27 @@ impl RepositoryHost { pub fn get_default_pkg_url_template( self, - ) -> Option + Clone + 'static> { + ) -> Option> + Clone + 'static> { use RepositoryHost::*; match self { GitHub => Some(apply_filenames_to_paths( - &[ - "{ repo }/releases/download/{ version }", - "{ repo }/releases/download/v{ version }", - ], + GITHUB_RELEASE_PATHS, &[FULL_FILENAMES, NOVERSION_FILENAMES], "", )), GitLab => Some(apply_filenames_to_paths( - &[ - "{ repo }/-/releases/{ version }/downloads/binaries", - "{ repo }/-/releases/v{ version }/downloads/binaries", - ], + GITLAB_RELEASE_PATHS, &[FULL_FILENAMES, NOVERSION_FILENAMES], "", )), BitBucket => Some(apply_filenames_to_paths( - &["{ repo }/downloads"], + BITBUCKET_RELEASE_PATHS, &[FULL_FILENAMES], "", )), SourceForge => Some(apply_filenames_to_paths( - &[ - "{ repo }/files/binaries/{ version }", - "{ repo }/files/binaries/v{ version }", - ], + SOURCEFORGE_RELEASE_PATHS, &[FULL_FILENAMES, NOVERSION_FILENAMES], "/download", )), @@ -84,13 +119,17 @@ impl RepositoryHost { } fn apply_filenames_to_paths( - paths: &'static [&'static str], - filenames: &'static [&'static [&'static str]], + paths: &'static [Template<'static>], + filenames: &'static [&'static [Template<'static>]], suffix: &'static str, -) -> impl Iterator + Clone + 'static { +) -> impl Iterator> + Clone + 'static { filenames .iter() .flat_map(|fs| fs.iter()) .cartesian_product(paths.iter()) - .map(move |(filename, path)| format!("{path}/{filename}{suffix}")) + .map(move |(filename, path)| { + let mut template = path.clone() + filename; + template += Item::Text(suffix); + template + }) } diff --git a/crates/binstalk/src/ops/resolve.rs b/crates/binstalk/src/ops/resolve.rs index fe9f42f8..0c98a6f3 100644 --- a/crates/binstalk/src/ops/resolve.rs +++ b/crates/binstalk/src/ops/resolve.rs @@ -9,9 +9,9 @@ use std::{ use cargo_toml::Manifest; use compact_str::{CompactString, ToCompactString}; use itertools::Itertools; +use leon::Template; use maybe_owned::MaybeOwned; use semver::{Version, VersionReq}; -use tinytemplate::TinyTemplate; use tokio::task::block_in_place; use tracing::{debug, info, instrument, warn}; @@ -304,15 +304,13 @@ fn collect_bin_files( .map(Cow::Borrowed) .unwrap_or_else(|| bins::infer_bin_dir_template(&bin_data, extracted_files)); - let mut tt = TinyTemplate::new(); - - tt.add_template("bin_dir", &bin_dir)?; + let template = Template::parse(&bin_dir)?; // Create bin_files let bin_files = package_info .binaries .iter() - .map(|bin| bins::BinFile::new(&bin_data, bin.name.as_str(), &tt, no_symlinks)) + .map(|bin| bins::BinFile::new(&bin_data, bin.name.as_str(), &template, no_symlinks)) .collect::, BinstallError>>()?; let mut source_set = BTreeSet::new(); diff --git a/crates/leon/src/lib.rs b/crates/leon/src/lib.rs index 17eeeeca..e58e41e8 100644 --- a/crates/leon/src/lib.rs +++ b/crates/leon/src/lib.rs @@ -59,7 +59,7 @@ //! # let template = Template::parse("hello {name}").unwrap(); //! assert_eq!( //! template.render( -//! &vals(|_key| Some("marcus".into())) +//! &&vals(|_key| Some("marcus".into())) //! ).unwrap().as_str(), //! "hello marcus", //! ); @@ -76,7 +76,7 @@ //! let mut buf: Vec = Vec::new(); //! template.render_into( //! &mut buf, -//! &vals(|key| if key == "name" { +//! &&vals(|key| if key == "name" { //! Some("julius".into()) //! } else { //! None @@ -107,7 +107,7 @@ //! name: &'static str, //! } //! impl Values for MyMap { -//! fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { +//! fn get_value(&self, key: &str) -> Option> { //! if key == "name" { //! Some(self.name.into()) //! } else { diff --git a/crates/leon/src/macros.rs b/crates/leon/src/macros.rs index 3833a3c3..c489765d 100644 --- a/crates/leon/src/macros.rs +++ b/crates/leon/src/macros.rs @@ -1,4 +1,35 @@ -/// Construct a template constant without needing to make an items constant. +#[doc(hidden)] +#[macro_export] +macro_rules! __template_item { + () => {}; + ({ $key:literal }) => { + $crate::Item::Key($key) + }; + ( $text:literal ) => { + $crate::Item::Text($text) + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __template_impl { + ($( $token:tt ),* ; $default:expr) => { + $crate::Template::new( + { + const ITEMS: &'static [$crate::Item<'static>] = &[ + $( + $crate::__template_item!($token) + ),* + ]; + ITEMS + }, + $default, + ) + }; +} + +/// Construct a template constant using syntax similar to the template to be +/// passed to [`Template::parse`]. /// /// This is essentially a shorthand for: /// @@ -13,9 +44,8 @@ /// # Examples /// /// ``` -/// use leon::Item::*; /// assert_eq!( -/// leon::template!(Text("Hello "), Key("name")) +/// leon::template!("Hello ", {"name"}) /// .render(&[("name", "Магда Нахман")]) /// .unwrap(), /// "Hello Магда Нахман", @@ -25,9 +55,8 @@ /// With a default: /// /// ``` -/// use leon::Item::*; /// assert_eq!( -/// leon::template!(Text("Hello "), Key("name"); "M. P. T. Acharya") +/// leon::template!("Hello ", {"name"}; "M. P. T. Acharya") /// .render(&[("city", "Madras")]) /// .unwrap(), /// "Hello M. P. T. Acharya", @@ -35,16 +64,93 @@ /// ``` #[macro_export] macro_rules! template { - ($($item:expr),* $(,)?) => { - $crate::Template::new({ - const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*]; - ITEMS - }, ::core::option::Option::None) + () => { + $crate::Template::new( + { + const ITEMS: &'static [$crate::Item<'static>] = &[]; + ITEMS + }, + ::core::option::Option::None, + ) }; - ($($item:expr),* $(,)? ; $default:expr) => { - $crate::Template::new({ - const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*]; - ITEMS - }, ::core::option::Option::Some($default)) + + ($( $token:tt ),* $(,)?) => { + $crate::__template_impl!($( $token ),* ; ::core::option::Option::None) + }; + + ($( $token:tt ),* $(,)? ; $default:expr) => { + $crate::__template_impl!($( $token ),* ; ::core::option::Option::Some($default)) }; } + +#[cfg(test)] +mod tests { + use crate::{template, Item, Template}; + + #[test] + fn test_template2() { + assert_eq!(template!(), Template::new(&[], None),); + + // Only literals + assert_eq!(template!("1"), Template::new(&[Item::Text("1")], None)); + + assert_eq!( + template!("1", "2"), + Template::new(&[Item::Text("1"), Item::Text("2")], None) + ); + + assert_eq!( + template!("1", "2", "3"), + Template::new(&[Item::Text("1"), Item::Text("2"), Item::Text("3")], None) + ); + + // Only keys + assert_eq!(template!({ "k1" }), Template::new(&[Item::Key("k1")], None)); + + assert_eq!( + template!({ "k1" }, { "k2" }), + Template::new(&[Item::Key("k1"), Item::Key("k2")], None) + ); + + assert_eq!( + template!({ "k1" }, { "k2" }, { "k3" }), + Template::new(&[Item::Key("k1"), Item::Key("k2"), Item::Key("k3")], None) + ); + + // Mixed + assert_eq!( + template!("1", { "k1" }, "3"), + Template::new(&[Item::Text("1"), Item::Key("k1"), Item::Text("3")], None) + ); + + assert_eq!( + template!("1", "2", { "k1" }, "3", "4"), + Template::new( + &[ + Item::Text("1"), + Item::Text("2"), + Item::Key("k1"), + Item::Text("3"), + Item::Text("4") + ], + None + ) + ); + + assert_eq!( + template!("1", "2", { "k1" }, { "k2" }, "3", "4", { "k3" }), + Template::new( + &[ + Item::Text("1"), + Item::Text("2"), + Item::Key("k1"), + Item::Key("k2"), + Item::Text("3"), + Item::Text("4"), + Item::Key("k3"), + ], + None + ) + ); + } +} diff --git a/crates/leon/src/parser.rs b/crates/leon/src/parser.rs index 963775fc..7c033cba 100644 --- a/crates/leon/src/parser.rs +++ b/crates/leon/src/parser.rs @@ -77,7 +77,7 @@ impl<'s> Template<'s> { #[cfg(test)] mod test_valid { - use crate::{template, Item::*, Template}; + use crate::{template, Template}; #[test] fn empty() { @@ -88,34 +88,31 @@ mod test_valid { #[test] fn no_keys() { let template = Template::parse("hello world").unwrap(); - assert_eq!(template, template!(Text("hello world"))); + assert_eq!(template, template!("hello world")); } #[test] fn leading_key() { let template = Template::parse("{salutation} world").unwrap(); - assert_eq!(template, template!(Key("salutation"), Text(" world"))); + assert_eq!(template, template!({ "salutation" }, " world")); } #[test] fn trailing_key() { let template = Template::parse("hello {name}").unwrap(); - assert_eq!(template, template!(Text("hello "), Key("name"))); + assert_eq!(template, template!("hello ", { "name" })); } #[test] fn middle_key() { let template = Template::parse("hello {name}!").unwrap(); - assert_eq!(template, template!(Text("hello "), Key("name"), Text("!"))); + assert_eq!(template, template!("hello ", { "name" }, "!")); } #[test] fn middle_text() { let template = Template::parse("{salutation} good {title}").unwrap(); - assert_eq!( - template, - template!(Key("salutation"), Text(" good "), Key("title")) - ); + assert_eq!(template, template!({ "salutation" }, " good ", { "title" })); } #[test] @@ -131,13 +128,13 @@ mod test_valid { assert_eq!( template, template!( - Text("\n And if thy native country was "), - Key("ancient civilisation"), - Text(",\n What need to slight thee? Came not "), - Key("hero"), - Text(" thence,\n Who gave to "), - Key("country"), - Text(" her books and art of writing?\n "), + "\n And if thy native country was ", + { "ancient civilisation" }, + ",\n What need to slight thee? Came not ", + { "hero" }, + " thence,\n Who gave to ", + { "country" }, + " her books and art of writing?\n ", ) ); } @@ -145,19 +142,19 @@ mod test_valid { #[test] fn key_no_whitespace() { let template = Template::parse("{word}").unwrap(); - assert_eq!(template, template!(Key("word"))); + assert_eq!(template, template!({ "word" })); } #[test] fn key_leading_whitespace() { let template = Template::parse("{ word}").unwrap(); - assert_eq!(template, template!(Key("word"))); + assert_eq!(template, template!({ "word" })); } #[test] fn key_trailing_whitespace() { let template = Template::parse("{word\n}").unwrap(); - assert_eq!(template, template!(Key("word"))); + assert_eq!(template, template!({ "word" })); } #[test] @@ -168,46 +165,31 @@ mod test_valid { }", ) .unwrap(); - assert_eq!(template, template!(Key("word"))); + assert_eq!(template, template!({ "word" })); } #[test] fn key_inner_whitespace() { let template = Template::parse("{ a word }").unwrap(); - assert_eq!(template, template!(Key("a word"))); + assert_eq!(template, template!({ "a word" })); } #[test] fn escape_left() { let template = Template::parse(r"this \{ single left brace").unwrap(); - assert_eq!( - template, - template!(Text("this "), Text("{"), Text(" single left brace")) - ); + assert_eq!(template, template!("this ", "{", " single left brace")); } #[test] fn escape_right() { let template = Template::parse(r"this \} single right brace").unwrap(); - assert_eq!( - template, - template!(Text("this "), Text("}"), Text(" single right brace")) - ); + assert_eq!(template, template!("this ", "}", " single right brace")); } #[test] fn escape_both() { let template = Template::parse(r"these \{ two \} braces").unwrap(); - assert_eq!( - template, - template!( - Text("these "), - Text("{"), - Text(" two "), - Text("}"), - Text(" braces") - ) - ); + assert_eq!(template, template!("these ", "{", " two ", "}", " braces")); } #[test] @@ -215,15 +197,7 @@ mod test_valid { let template = Template::parse(r"these \{\{ four \}\} braces").unwrap(); assert_eq!( template, - template!( - Text("these "), - Text("{"), - Text("{"), - Text(" four "), - Text("}"), - Text("}"), - Text(" braces") - ) + template!("these ", "{", "{", " four ", "}", "}", " braces") ); } @@ -232,13 +206,7 @@ mod test_valid { let template = Template::parse(r"these \\ backslashes \\\\").unwrap(); assert_eq!( template, - template!( - Text("these "), - Text(r"\"), - Text(" backslashes "), - Text(r"\"), - Text(r"\"), - ) + template!("these ", r"\", " backslashes ", r"\", r"\",) ); } @@ -247,16 +215,7 @@ mod test_valid { let template = Template::parse(r"\\{ a } \{{ b } \}{ c }").unwrap(); assert_eq!( template, - template!( - Text(r"\"), - Key("a"), - Text(" "), - Text(r"{"), - Key("b"), - Text(" "), - Text(r"}"), - Key("c"), - ) + template!(r"\", { "a" }, " ", r"{", { "b" }, " ", r"}", { "c" }) ); } @@ -265,44 +224,32 @@ mod test_valid { let template = Template::parse(r"{ a }\\ { b }\{ { c }\}").unwrap(); assert_eq!( template, - template!( - Key("a"), - Text(r"\"), - Text(" "), - Key("b"), - Text(r"{"), - Text(" "), - Key("c"), - Text(r"}"), - ) + template!({ "a" }, r"\", " ", { "b" }, r"{", " ", { "c" }, r"}") ); } #[test] fn multibyte_texts() { let template = Template::parse("幸徳 {particle} 秋水").unwrap(); - assert_eq!( - template, - template!(Text("幸徳 "), Key("particle"), Text(" 秋水")) - ); + assert_eq!(template, template!("幸徳 ", { "particle" }, " 秋水")); } #[test] fn multibyte_key() { let template = Template::parse("The { 連盟 }").unwrap(); - assert_eq!(template, template!(Text("The "), Key("連盟"))); + assert_eq!(template, template!("The ", { "連盟" })); } #[test] fn multibyte_both() { let template = Template::parse("大杉 {栄}").unwrap(); - assert_eq!(template, template!(Text("大杉 "), Key("栄"))); + assert_eq!(template, template!("大杉 ", { "栄" })); } #[test] fn multibyte_whitespace() { let template = Template::parse("岩佐 作{ 太 }郎").unwrap(); - assert_eq!(template, template!(Text("岩佐 作"), Key("太"), Text("郎"))); + assert_eq!(template, template!("岩佐 作", { "太" }, "郎")); } #[test] @@ -310,26 +257,20 @@ mod test_valid { let template = Template::parse(r"日本\{アナキスト\}連盟").unwrap(); assert_eq!( template, - template!( - Text("日本"), - Text(r"{"), - Text("アナキスト"), - Text(r"}"), - Text("連盟") - ) + template!("日本", r"{", "アナキスト", r"}", "連盟") ); } #[test] fn multibyte_rtl_text() { let template = Template::parse("محمد صايل").unwrap(); - assert_eq!(template, template!(Text("محمد صايل"))); + assert_eq!(template, template!("محمد صايل")); } #[test] fn multibyte_rtl_key() { let template = Template::parse("محمد {ريشة}").unwrap(); - assert_eq!(template, template!(Text("محمد "), Key("ريشة"))); + assert_eq!(template, template!("محمد ", { "ريشة" })); } } diff --git a/crates/leon/src/template.rs b/crates/leon/src/template.rs index bf32e92d..67a4466b 100644 --- a/crates/leon/src/template.rs +++ b/crates/leon/src/template.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, fmt::Display, io::Write, ops::Add}; +use std::{borrow::Cow, fmt::Display, io::Write, ops}; use crate::{ParseError, RenderError, Values}; @@ -138,17 +138,20 @@ impl<'s> Template<'s> { Ok(String::from_utf8(buf).unwrap()) } + /// If the template contains key `key`. pub fn has_key(&self, key: &str) -> bool { - self.has_keys(&[key]) + self.has_any_of_keys(&[key]) } - pub fn has_keys(&self, keys: &[&str]) -> bool { + /// If the template contains any one of the `keys`. + pub fn has_any_of_keys(&self, keys: &[&str]) -> bool { self.items.iter().any(|token| match token { Item::Key(k) => keys.contains(k), _ => false, }) } + /// Returns all keys in this template. pub fn keys(&self) -> impl Iterator { self.items.iter().filter_map(|token| match token { Item::Key(k) => Some(k), @@ -160,39 +163,116 @@ impl<'s> Template<'s> { pub fn set_default(&mut self, default: &dyn Display) { self.default = Some(Cow::Owned(default.to_string())); } + + /// Cast `Template<'s>` to `Template<'t>` where `'s` is a subtype of `'t`, + /// meaning that `Template<'s>` outlives `Template<'t>`. + pub fn cast<'t>(self) -> Template<'t> + where + 's: 't, + { + Template { + items: match self.items { + Cow::Owned(vec) => Cow::Owned(vec), + Cow::Borrowed(slice) => Cow::Borrowed(slice as &'t [Item<'t>]), + }, + default: self.default.map(|default| default as Cow<'t, str>), + } + } } -impl<'s> Add for Template<'s> { - type Output = Self; - - fn add(mut self, rhs: Self) -> Self::Output { +impl<'s, 'rhs: 's> ops::AddAssign<&Template<'rhs>> for Template<'s> { + fn add_assign(&mut self, rhs: &Template<'rhs>) { self.items .to_mut() .extend(rhs.items.as_ref().iter().cloned()); + + if let Some(default) = &rhs.default { + self.default = Some(default.clone()); + } + } +} + +impl<'s, 'rhs: 's> ops::AddAssign> for Template<'s> { + fn add_assign(&mut self, rhs: Template<'rhs>) { + match rhs.items { + Cow::Borrowed(items) => self.items.to_mut().extend(items.iter().cloned()), + Cow::Owned(items) => self.items.to_mut().extend(items.into_iter()), + } + if let Some(default) = rhs.default { self.default = Some(default); } + } +} + +impl<'s, 'item: 's> ops::AddAssign> for Template<'s> { + fn add_assign(&mut self, item: Item<'item>) { + self.items.to_mut().push(item); + } +} + +impl<'s, 'item: 's> ops::AddAssign<&Item<'item>> for Template<'s> { + fn add_assign(&mut self, item: &Item<'item>) { + self.add_assign(item.clone()) + } +} + +impl<'s, 'rhs: 's> ops::Add> for Template<'s> { + type Output = Self; + + fn add(mut self, rhs: Template<'rhs>) -> Self::Output { + self += rhs; + self + } +} + +impl<'s, 'rhs: 's> ops::Add<&Template<'rhs>> for Template<'s> { + type Output = Self; + + fn add(mut self, rhs: &Template<'rhs>) -> Self::Output { + self += rhs; + self + } +} + +impl<'s, 'item: 's> ops::Add> for Template<'s> { + type Output = Self; + + fn add(mut self, item: Item<'item>) -> Self::Output { + self += item; + self + } +} + +impl<'s, 'item: 's> ops::Add<&Item<'item>> for Template<'s> { + type Output = Self; + + fn add(mut self, item: &Item<'item>) -> Self::Output { + self += item; self } } #[cfg(test)] mod test { - use crate::Item::{Key, Text}; + use crate::Template; #[test] fn concat_templates() { - let t1 = crate::template!(Text("Hello"), Key("name")); - let t2 = crate::template!(Text("have a"), Key("adjective"), Text("day")); + let t1 = crate::template!("Hello", { "name" }); + let t2 = crate::template!("have a", { "adjective" }, "day"); assert_eq!( t1 + t2, - crate::template!( - Text("Hello"), - Key("name"), - Text("have a"), - Key("adjective"), - Text("day") - ), + crate::template!("Hello", { "name" }, "have a", { "adjective" }, "day"), ); } + + #[test] + fn test_cast() { + fn inner<'a>(_: &'a u32, _: Template<'a>) {} + + let template: Template<'static> = crate::template!("hello"); + let i = 1; + inner(&i, template.cast()); + } } diff --git a/crates/leon/src/values.rs b/crates/leon/src/values.rs index 3e7d8ca9..7641d8a8 100644 --- a/crates/leon/src/values.rs +++ b/crates/leon/src/values.rs @@ -5,14 +5,14 @@ use std::{ }; pub trait Values { - fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option>; + fn get_value(&self, key: &str) -> Option>; } impl Values for &T where T: Values, { - fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + fn get_value(&self, key: &str) -> Option> { T::get_value(self, key) } } @@ -22,7 +22,7 @@ where K: AsRef, V: AsRef, { - fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + fn get_value(&self, key: &str) -> Option> { self.iter().find_map(|(k, v)| { if k.as_ref() == key { Some(Cow::Borrowed(v.as_ref())) @@ -38,7 +38,7 @@ where K: AsRef, V: AsRef, { - fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + fn get_value(&self, key: &str) -> Option> { (*self).get_value(key) } } @@ -48,7 +48,7 @@ where K: AsRef, V: AsRef, { - fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + fn get_value(&self, key: &str) -> Option> { self.as_slice().get_value(key) } } @@ -58,7 +58,7 @@ where K: AsRef, V: AsRef, { - fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + fn get_value(&self, key: &str) -> Option> { self.as_slice().get_value(key) } } @@ -69,7 +69,7 @@ where V: AsRef, S: BuildHasher, { - fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + fn get_value(&self, key: &str) -> Option> { self.get(key).map(|v| Cow::Borrowed(v.as_ref())) } } @@ -79,7 +79,7 @@ where K: Borrow + Ord, V: AsRef, { - fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + fn get_value(&self, key: &str) -> Option> { self.get(key).map(|v| Cow::Borrowed(v.as_ref())) } } @@ -87,25 +87,22 @@ where /// Workaround to allow using functions as [`Values`]. /// /// As this isn't constructible you'll want to use [`vals()`] instead. -pub struct ValuesFn -where - F: for<'s> Fn(&'s str) -> Option> + Send + 'static, -{ +pub struct ValuesFn { inner: F, } -impl Values for ValuesFn +impl<'s, F> Values for &'s ValuesFn where - F: for<'s> Fn(&'s str) -> Option> + Send + 'static, + F: Fn(&str) -> Option> + 's, { - fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + fn get_value(&self, key: &str) -> Option> { (self.inner)(key) } } -impl From for ValuesFn +impl<'f, F> From for ValuesFn where - F: for<'s> Fn(&'s str) -> Option> + Send + 'static, + F: Fn(&str) -> Option> + 'f, { fn from(inner: F) -> Self { Self { inner } @@ -123,11 +120,11 @@ where /// /// fn use_values(_values: impl Values) {} /// -/// use_values(vals(|_| Some("hello".into()))); +/// use_values(&vals(|_| Some("hello".into()))); /// ``` -pub const fn vals(func: F) -> ValuesFn +pub const fn vals<'f, F>(func: F) -> ValuesFn where - F: for<'s> Fn(&'s str) -> Option> + Send + 'static, + F: Fn(&str) -> Option> + 'f, { ValuesFn { inner: func } }