Use leon for template in binstalk & detect malformed pkg-url/pkg-fmt early (#933)

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<Cow<'_, str>>;
   ```
   
   Now, `ValuesFn` also accepts non-`'static` function, but now
   `leon::Values` is only implemented for `&ValuesFn<F>` 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<Template<'rhs>> for Template<'s>`
* `impl<'s, 'item: 's> ops::AddAssign<Item<'item>> for Template<'s>`
* `impl<'s, 'item: 's> ops::AddAssign<&Item<'item>> for Template<'s>`
* `impl<'s, 'rhs: 's> ops::Add<Template<'rhs>> for Template<'s>` (improved existing `Add` impl)
* `impl<'s, 'rhs: 's> ops::Add<&Template<'rhs>> for Template<'s>`
* `impl<'s, 'item: 's> ops::Add<Item<'item>> for Template<'s>`
* `impl<'s, 'item: 's> ops::Add<&Item<'item>> for Template<'s>`

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
Co-authored-by: Félix Saparelli <felix@passcod.name>
This commit is contained in:
Jiahao XU 2023-03-26 16:11:10 +11:00 committed by GitHub
parent 47d4aeaa96
commit a27d5aebf6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 512 additions and 312 deletions

2
Cargo.lock generated
View file

@ -169,6 +169,7 @@ dependencies = [
"home", "home",
"itertools", "itertools",
"jobslot", "jobslot",
"leon",
"maybe-owned", "maybe-owned",
"miette", "miette",
"normalize-path", "normalize-path",
@ -178,7 +179,6 @@ dependencies = [
"strum", "strum",
"tempfile", "tempfile",
"thiserror", "thiserror",
"tinytemplate",
"tokio", "tokio",
"tracing", "tracing",
"url", "url",

View file

@ -46,14 +46,23 @@ impl PkgFmt {
/// List of possible file extensions for the format /// List of possible file extensions for the format
/// (with prefix `.`). /// (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 { match self {
PkgFmt::Tar => &[".tar"], PkgFmt::Tar => &[".tar"],
PkgFmt::Tbz2 => &[".tbz2", ".tar.bz2"], PkgFmt::Tbz2 => &[".tbz2", ".tar.bz2"],
PkgFmt::Tgz => &[".tgz", ".tar.gz"], PkgFmt::Tgz => &[".tgz", ".tar.gz"],
PkgFmt::Txz => &[".txz", ".tar.xz"], PkgFmt::Txz => &[".txz", ".tar.xz"],
PkgFmt::Tzstd => &[".tzstd", ".tzst", ".tar.zst"], PkgFmt::Tzstd => &[".tzstd", ".tzst", ".tar.zst"],
PkgFmt::Bin => &[".bin", ".exe", ""], PkgFmt::Bin => {
if is_windows {
&[".bin", "", ".exe"]
} else {
&[".bin", ""]
}
}
PkgFmt::Zip => &[".zip"], PkgFmt::Zip => &[".zip"],
} }
} }

View file

@ -21,6 +21,7 @@ either = "1.8.1"
home = "0.5.4" home = "0.5.4"
itertools = "0.10.5" itertools = "0.10.5"
jobslot = { version = "0.2.10", features = ["tokio"] } jobslot = { version = "0.2.10", features = ["tokio"] }
leon = { version = "1.0.0", path = "../leon" }
maybe-owned = "0.3.4" maybe-owned = "0.3.4"
miette = "5.6.0" miette = "5.6.0"
normalize-path = { version = "0.2.0", path = "../normalize-path" } normalize-path = { version = "0.2.0", path = "../normalize-path" }
@ -30,7 +31,6 @@ serde = { version = "1.0.157", features = ["derive"] }
strum = "0.24.1" strum = "0.24.1"
tempfile = "3.4.0" tempfile = "3.4.0"
thiserror = "1.0.40" thiserror = "1.0.40"
tinytemplate = "1.2.1"
# parking_lot for `tokio::sync::OnceCell::const_new` # parking_lot for `tokio::sync::OnceCell::const_new`
tokio = { version = "1.26.0", features = ["rt", "process", "sync", "signal", "parking_lot"], default-features = false } tokio = { version = "1.26.0", features = ["rt", "process", "sync", "signal", "parking_lot"], default-features = false }
tracing = "0.1.37" tracing = "0.1.37"

View file

@ -5,9 +5,8 @@ use std::{
}; };
use compact_str::{format_compact, CompactString}; use compact_str::{format_compact, CompactString};
use leon::Template;
use normalize_path::NormalizePath; use normalize_path::NormalizePath;
use serde::Serialize;
use tinytemplate::TinyTemplate;
use tracing::debug; use tracing::debug;
use crate::{ use crate::{
@ -80,7 +79,7 @@ impl BinFile {
pub fn new( pub fn new(
data: &Data<'_>, data: &Data<'_>,
base_name: &str, base_name: &str,
tt: &TinyTemplate, tt: &Template<'_>,
no_symlinks: bool, no_symlinks: bool,
) -> Result<Self, BinstallError> { ) -> Result<Self, BinstallError> {
let binary_ext = if data.target.contains("windows") { let binary_ext = if data.target.contains("windows") {
@ -95,7 +94,6 @@ impl BinFile {
target: data.target, target: data.target,
version: data.version, version: data.version,
bin: base_name, bin: base_name,
format: binary_ext,
binary_ext, binary_ext,
}; };
@ -107,7 +105,7 @@ impl BinFile {
} else { } else {
// Generate install paths // Generate install paths
// Source path is the download dir + the generated binary path // 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(); let path_normalized = Path::new(&path).normalize();
@ -238,7 +236,7 @@ pub struct Data<'a> {
pub install_path: &'a Path, pub install_path: &'a Path,
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug)]
struct Context<'c> { struct Context<'c> {
pub name: &'c str, pub name: &'c str,
pub repo: Option<&'c str>, pub repo: Option<&'c str>,
@ -246,18 +244,23 @@ struct Context<'c> {
pub version: &'c str, pub version: &'c str,
pub bin: &'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 /// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise
#[serde(rename = "binary-ext")]
pub binary_ext: &'c str, pub binary_ext: &'c str,
} }
impl<'c> Context<'c> { impl leon::Values for Context<'_> {
/// * `tt` - must have a template with name "bin_dir" fn get_value<'s>(&'s self, key: &str) -> Option<Cow<'s, str>> {
fn render_with_compiled_tt(&self, tt: &TinyTemplate) -> Result<String, BinstallError> { match key {
Ok(tt.render("bin_dir", self)?) "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,
}
} }
} }

View file

@ -11,7 +11,6 @@ use cargo_toml::Error as CargoTomlError;
use compact_str::CompactString; use compact_str::CompactString;
use miette::{Diagnostic, Report}; use miette::{Diagnostic, Report};
use thiserror::Error; use thiserror::Error;
use tinytemplate::error::Error as TinyTemplateError;
use tokio::task; use tokio::task;
use tracing::{error, warn}; use tracing::{error, warn};
@ -82,13 +81,33 @@ pub enum BinstallError {
#[diagnostic(severity(error), code(binstall::url_parse))] #[diagnostic(severity(error), code(binstall::url_parse))]
UrlParse(#[from] url::ParseError), UrlParse(#[from] url::ParseError),
/// A rendering error in a template. /// Failed to parse template.
/// ///
/// - Code: `binstall::template` /// - Code: `binstall::template`
/// - Exit: 67 /// - 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}")] #[error("Failed to render template: {0}")]
#[diagnostic(severity(error), code(binstall::template))] #[diagnostic(severity(error), code(binstall::template))]
Template(Box<TinyTemplateError>), #[source_code(transparent)]
#[label(transparent)]
TemplateRenderError(
#[from]
#[diagnostic_source]
leon::RenderError,
),
/// Failed to download or failed to decode the body. /// Failed to download or failed to decode the body.
/// ///
@ -308,7 +327,8 @@ impl BinstallError {
TaskJoinError(_) => 17, TaskJoinError(_) => 17,
UserAbort => 32, UserAbort => 32,
UrlParse(_) => 65, UrlParse(_) => 65,
Template(_) => 67, TemplateParseError(..) => 67,
TemplateRenderError(..) => 69,
Download(_) => 68, Download(_) => 68,
SubProcess { .. } => 70, SubProcess { .. } => 70,
Io(_) => 74, Io(_) => 74,
@ -404,12 +424,6 @@ impl From<RemoteError> for BinstallError {
} }
} }
impl From<TinyTemplateError> for BinstallError {
fn from(e: TinyTemplateError) -> Self {
BinstallError::Template(Box::new(e))
}
}
impl From<CargoTomlError> for BinstallError { impl From<CargoTomlError> for BinstallError {
fn from(e: CargoTomlError) -> Self { fn from(e: CargoTomlError) -> Self {
BinstallError::CargoManifest(Box::new(e)) BinstallError::CargoManifest(Box::new(e))

View file

@ -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 compact_str::{CompactString, ToCompactString};
use either::Either; use either::Either;
use itertools::Itertools; use leon::Template;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::Serialize;
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use tinytemplate::TinyTemplate;
use tracing::{debug, warn}; use tracing::{debug, warn};
use url::Url; use url::Url;
@ -35,36 +33,41 @@ pub struct GhCrateMeta {
resolution: OnceCell<(Url, PkgFmt)>, resolution: OnceCell<(Url, PkgFmt)>,
} }
type FindTaskRes = Result<Option<(Url, PkgFmt)>, BinstallError>;
impl GhCrateMeta { impl GhCrateMeta {
/// * `tt` - must have added a template named "pkg_url". fn launch_baseline_find_tasks(
fn launch_baseline_find_tasks<'a>( &self,
&'a self, futures_resolver: &FuturesResolver<(Url, PkgFmt), BinstallError>,
pkg_fmt: PkgFmt, pkg_fmt: PkgFmt,
tt: &'a TinyTemplate, pkg_url: &Template<'_>,
pkg_url: &'a str, repo: Option<&str>,
repo: Option<&'a str>, ) {
) -> impl Iterator<Item = impl Future<Output = FindTaskRes> + 'static> + 'a { let render_url = |ext| {
// build up list of potential URLs let ctx = Context::from_data_with_repo(&self.data, &self.target_data.target, ext, repo);
let urls = pkg_fmt match ctx.render_url_with_compiled_tt(pkg_url) {
.extensions() Ok(url) => Some(url),
.iter() Err(err) => {
.filter_map(move |ext| { warn!("Failed to render url for {ctx:#?}: {err}");
let ctx = None
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
}
} }
}) }
.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 // go check all potential URLs at once
urls.map(move |url| { futures_resolver.extend(urls.map(move |url| {
let client = self.client.clone(); let client = self.client.clone();
let gh_api_client = self.gh_api_client.clone(); let gh_api_client = self.gh_api_client.clone();
@ -73,7 +76,7 @@ impl GhCrateMeta {
.await? .await?
.then_some((url, pkg_fmt))) .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 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 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() if pkg_fmt.is_none()
&& !(pkg_url.contains("format") && !template.has_any_of_keys(&["format", "archive-format", "archive-suffix"])
|| pkg_url.contains("archive-format")
|| pkg_url.contains("archive-suffix"))
{ {
// The crate does not specify the pkg-fmt, yet its pkg-url // The crate does not specify the pkg-fmt, yet its pkg-url
// template doesn't contains format, archive-format or // template doesn't contains format, archive-format or
@ -115,23 +118,36 @@ impl super::Fetcher for GhCrateMeta {
// just a best-effort // just a best-effort
pkg_fmt = PkgFmt::guess_pkg_format(pkg_url); 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() { if pkg_fmt.is_none() {
return Err(InvalidPkgFmtError { return Err(InvalidPkgFmtError {
crate_name: self.data.name.clone(), crate_name: crate_name.clone(),
version: self.data.version.clone(), version: version.clone(),
target: self.target_data.target.clone(), target: target.clone(),
pkg_url: pkg_url.to_string(), 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", 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()); .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() { } else if let Some(repo) = repo.as_ref() {
if let Some(pkg_urls) = if let Some(pkg_urls) =
RepositoryHost::guess_git_hosting_services(repo)?.get_default_pkg_url_template() 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 { } else {
warn!( warn!(
concat!( concat!(
@ -172,16 +188,12 @@ impl super::Fetcher for GhCrateMeta {
// Iterate over pkg_urls first to avoid String::clone. // Iterate over pkg_urls first to avoid String::clone.
for pkg_url in pkg_urls { 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 // Clone iter pkg_fmts to ensure all pkg_fmts is
// iterated over for each pkg_url, which is // iterated over for each pkg_url, which is
// basically cartesian product. // basically cartesian product.
// | // |
for pkg_fmt in pkg_fmts.clone() { 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<ExtractedFiles, BinstallError> { async fn fetch_and_extract(&self, dst: &Path) -> Result<ExtractedFiles, BinstallError> {
let (url, pkg_fmt) = self.resolution.get().unwrap(); // find() is called first 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()) Ok(Download::new(self.client.clone(), url.clone())
.and_extract(*pkg_fmt, dst) .and_extract(*pkg_fmt, dst)
.await?) .await?)
@ -242,50 +257,67 @@ impl super::Fetcher for GhCrateMeta {
} }
/// Template for constructing download paths /// Template for constructing download paths
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug)]
struct Context<'c> { struct Context<'c> {
pub name: &'c str, pub name: &'c str,
pub repo: Option<&'c str>, pub repo: Option<&'c str>,
pub target: &'c str, pub target: &'c str,
pub version: &'c str, pub version: &'c str,
/// Soft-deprecated alias for archive-format
pub format: &'c str,
/// Archive format e.g. tar.gz, zip /// Archive format e.g. tar.gz, zip
#[serde(rename = "archive-format")] pub archive_format: Option<&'c str>,
pub archive_format: &'c str,
#[serde(rename = "archive-suffix")] pub archive_suffix: Option<&'c str>,
pub archive_suffix: &'c str,
/// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise /// Filename extension on the binary, i.e. .exe on Windows, nothing otherwise
#[serde(rename = "binary-ext")]
pub binary_ext: &'c str, pub binary_ext: &'c str,
} }
impl leon::Values for Context<'_> {
fn get_value<'s>(&'s self, key: &str) -> Option<Cow<'s, str>> {
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> { impl<'c> Context<'c> {
pub(self) fn from_data_with_repo( pub(self) fn from_data_with_repo(
data: &'c Data, data: &'c Data,
target: &'c str, target: &'c str,
archive_suffix: &'c str, archive_suffix: Option<&'c str>,
repo: Option<&'c str>, repo: Option<&'c str>,
) -> Self { ) -> Self {
let archive_format = if archive_suffix.is_empty() { let archive_format = archive_suffix.map(|archive_suffix| {
// Empty archive_suffix means PkgFmt::Bin if archive_suffix.is_empty() {
"bin" // Empty archive_suffix means PkgFmt::Bin
} else { "bin"
debug_assert!(archive_suffix.starts_with('.'), "{archive_suffix}"); } else {
debug_assert!(archive_suffix.starts_with('.'), "{archive_suffix}");
&archive_suffix[1..] &archive_suffix[1..]
}; }
});
Self { Self {
name: &data.name, name: &data.name,
repo, repo,
target, target,
version: &data.version, version: &data.version,
format: archive_format,
archive_format, archive_format,
archive_suffix, archive_suffix,
binary_ext: if target.contains("windows") { binary_ext: if target.contains("windows") {
@ -298,31 +330,31 @@ impl<'c> Context<'c> {
#[cfg(test)] #[cfg(test)]
pub(self) fn from_data(data: &'c Data, target: &'c str, archive_format: &'c str) -> Self { 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". /// * `tt` - must have added a template named "pkg_url".
pub(self) fn render_url_with_compiled_tt( pub(self) fn render_url_with_compiled_tt(
&self, &self,
tt: &TinyTemplate, tt: &Template<'_>,
template: &str,
) -> Result<Url, BinstallError> { ) -> Result<Url, BinstallError> {
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)] #[cfg(test)]
pub(self) fn render_url(&self, template: &str) -> Result<Url, BinstallError> { pub(self) fn render_url(&self, template: &str) -> Result<Url, BinstallError> {
let mut tt = TinyTemplate::new(); debug!("Render {template} using context in render_url: {self:?}");
tt.add_template("pkg_url", template)?;
self.render_url_with_compiled_tt(&tt, template) let tt = Template::parse(template)?;
self.render_url_with_compiled_tt(&tt)
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::manifests::cargo_toml_binstall::{PkgFmt, PkgMeta}; use crate::manifests::cargo_toml_binstall::PkgMeta;
use super::{super::Data, Context}; use super::{super::Data, Context};
use compact_str::ToCompactString; use compact_str::ToCompactString;
@ -365,10 +397,7 @@ mod test {
#[test] #[test]
fn no_repo_but_full_url() { fn no_repo_but_full_url() {
let meta = PkgMeta { let pkg_url = &format!("https://example.com{}", &DEFAULT_PKG_URL[8..]);
pkg_url: Some(format!("https://example.com{DEFAULT_PKG_URL}")),
..Default::default()
};
let data = Data::new( let data = Data::new(
"cargo-binstall".to_compact_string(), "cargo-binstall".to_compact_string(),
@ -378,19 +407,15 @@ mod test {
let ctx = Context::from_data(&data, "x86_64-unknown-linux-gnu", ".tgz"); let ctx = Context::from_data(&data, "x86_64-unknown-linux-gnu", ".tgz");
assert_eq!( 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") url("https://example.com/releases/download/v1.2.3/cargo-binstall-x86_64-unknown-linux-gnu-v1.2.3.tgz")
); );
} }
#[test] #[test]
fn different_url() { fn different_url() {
let meta = PkgMeta { let pkg_url =
pkg_url: Some( "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ archive-format }";
"{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ archive-format }"
.to_string()),
..Default::default()
};
let data = Data::new( let data = Data::new(
"radio-sx128x".to_compact_string(), "radio-sx128x".to_compact_string(),
@ -400,17 +425,14 @@ mod test {
let ctx = Context::from_data(&data, "x86_64-unknown-linux-gnu", ".tgz"); let ctx = Context::from_data(&data, "x86_64-unknown-linux-gnu", ".tgz");
assert_eq!( 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") 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] #[test]
fn deprecated_format() { fn deprecated_format() {
let meta = PkgMeta { let pkg_url = "{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ format }";
pkg_url: Some("{ repo }/releases/download/v{ version }/sx128x-util-{ target }-v{ version }.{ format }".to_string()),
..Default::default()
};
let data = Data::new( let data = Data::new(
"radio-sx128x".to_compact_string(), "radio-sx128x".to_compact_string(),
@ -420,21 +442,15 @@ mod test {
let ctx = Context::from_data(&data, "x86_64-unknown-linux-gnu", ".tgz"); let ctx = Context::from_data(&data, "x86_64-unknown-linux-gnu", ".tgz");
assert_eq!( 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") 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] #[test]
fn different_ext() { fn different_ext() {
let meta = PkgMeta { let pkg_url =
pkg_url: Some( "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }.tar.xz";
"{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }.tar.xz"
.to_string(),
),
pkg_fmt: Some(PkgFmt::Txz),
..Default::default()
};
let data = Data::new( let data = Data::new(
"cargo-watch".to_compact_string(), "cargo-watch".to_compact_string(),
@ -444,18 +460,15 @@ mod test {
let ctx = Context::from_data(&data, "aarch64-apple-darwin", ".txz"); let ctx = Context::from_data(&data, "aarch64-apple-darwin", ".txz");
assert_eq!( 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") url("https://github.com/watchexec/cargo-watch/releases/download/v9.0.0/cargo-watch-v9.0.0-aarch64-apple-darwin.tar.xz")
); );
} }
#[test] #[test]
fn no_archive() { fn no_archive() {
let meta = PkgMeta { let pkg_url = "{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }{ binary-ext }"
pkg_url: Some("{ repo }/releases/download/v{ version }/{ name }-v{ version }-{ target }{ binary-ext }".to_string()), ;
pkg_fmt: Some(PkgFmt::Bin),
..Default::default()
};
let data = Data::new( let data = Data::new(
"cargo-watch".to_compact_string(), "cargo-watch".to_compact_string(),
@ -465,7 +478,7 @@ mod test {
let ctx = Context::from_data(&data, "aarch64-pc-windows-msvc", ".bin"); let ctx = Context::from_data(&data, "aarch64-pc-windows-msvc", ".bin");
assert_eq!( 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") url("https://github.com/watchexec/cargo-watch/releases/download/v9.0.0/cargo-watch-v9.0.0-aarch64-pc-windows-msvc.exe")
); );
} }

View file

@ -1,4 +1,5 @@
use itertools::Itertools; use itertools::Itertools;
use leon::{template, Item, Template};
use url::Url; use url::Url;
use crate::errors::BinstallError; use crate::errors::BinstallError;
@ -14,20 +15,63 @@ pub enum RepositoryHost {
/// Make sure to update possible_dirs in `bins::infer_bin_dir_template` /// Make sure to update possible_dirs in `bins::infer_bin_dir_template`
/// if you modified FULL_FILENAMES or NOVERSION_FILENAMES. /// if you modified FULL_FILENAMES or NOVERSION_FILENAMES.
pub const FULL_FILENAMES: &[&str] = &[ pub const FULL_FILENAMES: &[Template<'_>] = &[
"{ name }-{ target }-v{ version }{ archive-suffix }", template!("/", { "name" }, "-", { "target" }, "-v", { "version" }, {
"{ name }-{ target }-{ version }{ archive-suffix }", "archive-suffix"
"{ name }-{ version }-{ target }{ archive-suffix }", }),
"{ name }-v{ version }-{ target }{ archive-suffix }", template!("/", { "name" }, "-", { "target" }, "-", { "version" }, {
"{ name }_{ target }_v{ version }{ archive-suffix }", "archive-suffix"
"{ name }_{ target }_{ version }{ archive-suffix }", }),
"{ name }_{ version }_{ target }{ archive-suffix }", template!("/", { "name" }, "-", { "version" }, "-", { "target" }, {
"{ name }_v{ version }_{ target }{ archive-suffix }", "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] = &[ pub const NOVERSION_FILENAMES: &[Template<'_>] = &[
"{ name }-{ target }{ archive-suffix }", template!("/", { "name" }, "-", { "target" }, { "archive-suffix" }),
"{ 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 { impl RepositoryHost {
@ -45,36 +89,27 @@ impl RepositoryHost {
pub fn get_default_pkg_url_template( pub fn get_default_pkg_url_template(
self, self,
) -> Option<impl Iterator<Item = String> + Clone + 'static> { ) -> Option<impl Iterator<Item = Template<'static>> + Clone + 'static> {
use RepositoryHost::*; use RepositoryHost::*;
match self { match self {
GitHub => Some(apply_filenames_to_paths( GitHub => Some(apply_filenames_to_paths(
&[ GITHUB_RELEASE_PATHS,
"{ repo }/releases/download/{ version }",
"{ repo }/releases/download/v{ version }",
],
&[FULL_FILENAMES, NOVERSION_FILENAMES], &[FULL_FILENAMES, NOVERSION_FILENAMES],
"", "",
)), )),
GitLab => Some(apply_filenames_to_paths( GitLab => Some(apply_filenames_to_paths(
&[ GITLAB_RELEASE_PATHS,
"{ repo }/-/releases/{ version }/downloads/binaries",
"{ repo }/-/releases/v{ version }/downloads/binaries",
],
&[FULL_FILENAMES, NOVERSION_FILENAMES], &[FULL_FILENAMES, NOVERSION_FILENAMES],
"", "",
)), )),
BitBucket => Some(apply_filenames_to_paths( BitBucket => Some(apply_filenames_to_paths(
&["{ repo }/downloads"], BITBUCKET_RELEASE_PATHS,
&[FULL_FILENAMES], &[FULL_FILENAMES],
"", "",
)), )),
SourceForge => Some(apply_filenames_to_paths( SourceForge => Some(apply_filenames_to_paths(
&[ SOURCEFORGE_RELEASE_PATHS,
"{ repo }/files/binaries/{ version }",
"{ repo }/files/binaries/v{ version }",
],
&[FULL_FILENAMES, NOVERSION_FILENAMES], &[FULL_FILENAMES, NOVERSION_FILENAMES],
"/download", "/download",
)), )),
@ -84,13 +119,17 @@ impl RepositoryHost {
} }
fn apply_filenames_to_paths( fn apply_filenames_to_paths(
paths: &'static [&'static str], paths: &'static [Template<'static>],
filenames: &'static [&'static [&'static str]], filenames: &'static [&'static [Template<'static>]],
suffix: &'static str, suffix: &'static str,
) -> impl Iterator<Item = String> + Clone + 'static { ) -> impl Iterator<Item = Template<'static>> + Clone + 'static {
filenames filenames
.iter() .iter()
.flat_map(|fs| fs.iter()) .flat_map(|fs| fs.iter())
.cartesian_product(paths.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
})
} }

View file

@ -9,9 +9,9 @@ use std::{
use cargo_toml::Manifest; use cargo_toml::Manifest;
use compact_str::{CompactString, ToCompactString}; use compact_str::{CompactString, ToCompactString};
use itertools::Itertools; use itertools::Itertools;
use leon::Template;
use maybe_owned::MaybeOwned; use maybe_owned::MaybeOwned;
use semver::{Version, VersionReq}; use semver::{Version, VersionReq};
use tinytemplate::TinyTemplate;
use tokio::task::block_in_place; use tokio::task::block_in_place;
use tracing::{debug, info, instrument, warn}; use tracing::{debug, info, instrument, warn};
@ -304,15 +304,13 @@ fn collect_bin_files(
.map(Cow::Borrowed) .map(Cow::Borrowed)
.unwrap_or_else(|| bins::infer_bin_dir_template(&bin_data, extracted_files)); .unwrap_or_else(|| bins::infer_bin_dir_template(&bin_data, extracted_files));
let mut tt = TinyTemplate::new(); let template = Template::parse(&bin_dir)?;
tt.add_template("bin_dir", &bin_dir)?;
// Create bin_files // Create bin_files
let bin_files = package_info let bin_files = package_info
.binaries .binaries
.iter() .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::<Result<Vec<_>, BinstallError>>()?; .collect::<Result<Vec<_>, BinstallError>>()?;
let mut source_set = BTreeSet::new(); let mut source_set = BTreeSet::new();

View file

@ -59,7 +59,7 @@
//! # let template = Template::parse("hello {name}").unwrap(); //! # let template = Template::parse("hello {name}").unwrap();
//! assert_eq!( //! assert_eq!(
//! template.render( //! template.render(
//! &vals(|_key| Some("marcus".into())) //! &&vals(|_key| Some("marcus".into()))
//! ).unwrap().as_str(), //! ).unwrap().as_str(),
//! "hello marcus", //! "hello marcus",
//! ); //! );
@ -76,7 +76,7 @@
//! let mut buf: Vec<u8> = Vec::new(); //! let mut buf: Vec<u8> = Vec::new();
//! template.render_into( //! template.render_into(
//! &mut buf, //! &mut buf,
//! &vals(|key| if key == "name" { //! &&vals(|key| if key == "name" {
//! Some("julius".into()) //! Some("julius".into())
//! } else { //! } else {
//! None //! None
@ -107,7 +107,7 @@
//! name: &'static str, //! name: &'static str,
//! } //! }
//! impl Values for MyMap { //! impl Values for MyMap {
//! fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> { //! fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
//! if key == "name" { //! if key == "name" {
//! Some(self.name.into()) //! Some(self.name.into())
//! } else { //! } else {

View file

@ -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: /// This is essentially a shorthand for:
/// ///
@ -13,9 +44,8 @@
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use leon::Item::*;
/// assert_eq!( /// assert_eq!(
/// leon::template!(Text("Hello "), Key("name")) /// leon::template!("Hello ", {"name"})
/// .render(&[("name", "Магда Нахман")]) /// .render(&[("name", "Магда Нахман")])
/// .unwrap(), /// .unwrap(),
/// "Hello Магда Нахман", /// "Hello Магда Нахман",
@ -25,9 +55,8 @@
/// With a default: /// With a default:
/// ///
/// ``` /// ```
/// use leon::Item::*;
/// assert_eq!( /// assert_eq!(
/// leon::template!(Text("Hello "), Key("name"); "M. P. T. Acharya") /// leon::template!("Hello ", {"name"}; "M. P. T. Acharya")
/// .render(&[("city", "Madras")]) /// .render(&[("city", "Madras")])
/// .unwrap(), /// .unwrap(),
/// "Hello M. P. T. Acharya", /// "Hello M. P. T. Acharya",
@ -35,16 +64,93 @@
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! template { macro_rules! template {
($($item:expr),* $(,)?) => { () => {
$crate::Template::new({ $crate::Template::new(
const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*]; {
ITEMS const ITEMS: &'static [$crate::Item<'static>] = &[];
}, ::core::option::Option::None) ITEMS
},
::core::option::Option::None,
)
}; };
($($item:expr),* $(,)? ; $default:expr) => {
$crate::Template::new({ ($( $token:tt ),* $(,)?) => {
const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*]; $crate::__template_impl!($( $token ),* ; ::core::option::Option::None)
ITEMS };
}, ::core::option::Option::Some($default))
($( $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
)
);
}
}

View file

@ -77,7 +77,7 @@ impl<'s> Template<'s> {
#[cfg(test)] #[cfg(test)]
mod test_valid { mod test_valid {
use crate::{template, Item::*, Template}; use crate::{template, Template};
#[test] #[test]
fn empty() { fn empty() {
@ -88,34 +88,31 @@ mod test_valid {
#[test] #[test]
fn no_keys() { fn no_keys() {
let template = Template::parse("hello world").unwrap(); let template = Template::parse("hello world").unwrap();
assert_eq!(template, template!(Text("hello world"))); assert_eq!(template, template!("hello world"));
} }
#[test] #[test]
fn leading_key() { fn leading_key() {
let template = Template::parse("{salutation} world").unwrap(); let template = Template::parse("{salutation} world").unwrap();
assert_eq!(template, template!(Key("salutation"), Text(" world"))); assert_eq!(template, template!({ "salutation" }, " world"));
} }
#[test] #[test]
fn trailing_key() { fn trailing_key() {
let template = Template::parse("hello {name}").unwrap(); let template = Template::parse("hello {name}").unwrap();
assert_eq!(template, template!(Text("hello "), Key("name"))); assert_eq!(template, template!("hello ", { "name" }));
} }
#[test] #[test]
fn middle_key() { fn middle_key() {
let template = Template::parse("hello {name}!").unwrap(); let template = Template::parse("hello {name}!").unwrap();
assert_eq!(template, template!(Text("hello "), Key("name"), Text("!"))); assert_eq!(template, template!("hello ", { "name" }, "!"));
} }
#[test] #[test]
fn middle_text() { fn middle_text() {
let template = Template::parse("{salutation} good {title}").unwrap(); let template = Template::parse("{salutation} good {title}").unwrap();
assert_eq!( assert_eq!(template, template!({ "salutation" }, " good ", { "title" }));
template,
template!(Key("salutation"), Text(" good "), Key("title"))
);
} }
#[test] #[test]
@ -131,13 +128,13 @@ mod test_valid {
assert_eq!( assert_eq!(
template, template,
template!( template!(
Text("\n And if thy native country was "), "\n And if thy native country was ",
Key("ancient civilisation"), { "ancient civilisation" },
Text(",\n What need to slight thee? Came not "), ",\n What need to slight thee? Came not ",
Key("hero"), { "hero" },
Text(" thence,\n Who gave to "), " thence,\n Who gave to ",
Key("country"), { "country" },
Text(" her books and art of writing?\n "), " her books and art of writing?\n ",
) )
); );
} }
@ -145,19 +142,19 @@ mod test_valid {
#[test] #[test]
fn key_no_whitespace() { fn key_no_whitespace() {
let template = Template::parse("{word}").unwrap(); let template = Template::parse("{word}").unwrap();
assert_eq!(template, template!(Key("word"))); assert_eq!(template, template!({ "word" }));
} }
#[test] #[test]
fn key_leading_whitespace() { fn key_leading_whitespace() {
let template = Template::parse("{ word}").unwrap(); let template = Template::parse("{ word}").unwrap();
assert_eq!(template, template!(Key("word"))); assert_eq!(template, template!({ "word" }));
} }
#[test] #[test]
fn key_trailing_whitespace() { fn key_trailing_whitespace() {
let template = Template::parse("{word\n}").unwrap(); let template = Template::parse("{word\n}").unwrap();
assert_eq!(template, template!(Key("word"))); assert_eq!(template, template!({ "word" }));
} }
#[test] #[test]
@ -168,46 +165,31 @@ mod test_valid {
}", }",
) )
.unwrap(); .unwrap();
assert_eq!(template, template!(Key("word"))); assert_eq!(template, template!({ "word" }));
} }
#[test] #[test]
fn key_inner_whitespace() { fn key_inner_whitespace() {
let template = Template::parse("{ a word }").unwrap(); let template = Template::parse("{ a word }").unwrap();
assert_eq!(template, template!(Key("a word"))); assert_eq!(template, template!({ "a word" }));
} }
#[test] #[test]
fn escape_left() { fn escape_left() {
let template = Template::parse(r"this \{ single left brace").unwrap(); let template = Template::parse(r"this \{ single left brace").unwrap();
assert_eq!( assert_eq!(template, template!("this ", "{", " single left brace"));
template,
template!(Text("this "), Text("{"), Text(" single left brace"))
);
} }
#[test] #[test]
fn escape_right() { fn escape_right() {
let template = Template::parse(r"this \} single right brace").unwrap(); let template = Template::parse(r"this \} single right brace").unwrap();
assert_eq!( assert_eq!(template, template!("this ", "}", " single right brace"));
template,
template!(Text("this "), Text("}"), Text(" single right brace"))
);
} }
#[test] #[test]
fn escape_both() { fn escape_both() {
let template = Template::parse(r"these \{ two \} braces").unwrap(); let template = Template::parse(r"these \{ two \} braces").unwrap();
assert_eq!( assert_eq!(template, template!("these ", "{", " two ", "}", " braces"));
template,
template!(
Text("these "),
Text("{"),
Text(" two "),
Text("}"),
Text(" braces")
)
);
} }
#[test] #[test]
@ -215,15 +197,7 @@ mod test_valid {
let template = Template::parse(r"these \{\{ four \}\} braces").unwrap(); let template = Template::parse(r"these \{\{ four \}\} braces").unwrap();
assert_eq!( assert_eq!(
template, template,
template!( template!("these ", "{", "{", " four ", "}", "}", " braces")
Text("these "),
Text("{"),
Text("{"),
Text(" four "),
Text("}"),
Text("}"),
Text(" braces")
)
); );
} }
@ -232,13 +206,7 @@ mod test_valid {
let template = Template::parse(r"these \\ backslashes \\\\").unwrap(); let template = Template::parse(r"these \\ backslashes \\\\").unwrap();
assert_eq!( assert_eq!(
template, template,
template!( template!("these ", r"\", " backslashes ", r"\", r"\",)
Text("these "),
Text(r"\"),
Text(" backslashes "),
Text(r"\"),
Text(r"\"),
)
); );
} }
@ -247,16 +215,7 @@ mod test_valid {
let template = Template::parse(r"\\{ a } \{{ b } \}{ c }").unwrap(); let template = Template::parse(r"\\{ a } \{{ b } \}{ c }").unwrap();
assert_eq!( assert_eq!(
template, template,
template!( template!(r"\", { "a" }, " ", r"{", { "b" }, " ", r"}", { "c" })
Text(r"\"),
Key("a"),
Text(" "),
Text(r"{"),
Key("b"),
Text(" "),
Text(r"}"),
Key("c"),
)
); );
} }
@ -265,44 +224,32 @@ mod test_valid {
let template = Template::parse(r"{ a }\\ { b }\{ { c }\}").unwrap(); let template = Template::parse(r"{ a }\\ { b }\{ { c }\}").unwrap();
assert_eq!( assert_eq!(
template, template,
template!( template!({ "a" }, r"\", " ", { "b" }, r"{", " ", { "c" }, r"}")
Key("a"),
Text(r"\"),
Text(" "),
Key("b"),
Text(r"{"),
Text(" "),
Key("c"),
Text(r"}"),
)
); );
} }
#[test] #[test]
fn multibyte_texts() { fn multibyte_texts() {
let template = Template::parse("幸徳 {particle} 秋水").unwrap(); let template = Template::parse("幸徳 {particle} 秋水").unwrap();
assert_eq!( assert_eq!(template, template!("幸徳 ", { "particle" }, " 秋水"));
template,
template!(Text("幸徳 "), Key("particle"), Text(" 秋水"))
);
} }
#[test] #[test]
fn multibyte_key() { fn multibyte_key() {
let template = Template::parse("The { 連盟 }").unwrap(); let template = Template::parse("The { 連盟 }").unwrap();
assert_eq!(template, template!(Text("The "), Key("連盟"))); assert_eq!(template, template!("The ", { "連盟" }));
} }
#[test] #[test]
fn multibyte_both() { fn multibyte_both() {
let template = Template::parse("大杉 {栄}").unwrap(); let template = Template::parse("大杉 {栄}").unwrap();
assert_eq!(template, template!(Text("大杉 "), Key(""))); assert_eq!(template, template!("大杉 ", { "" }));
} }
#[test] #[test]
fn multibyte_whitespace() { fn multibyte_whitespace() {
let template = Template::parse("岩佐 作{ 太 }郎").unwrap(); let template = Template::parse("岩佐 作{ 太 }郎").unwrap();
assert_eq!(template, template!(Text("岩佐 作"), Key(""), Text(""))); assert_eq!(template, template!("岩佐 作", { "" }, ""));
} }
#[test] #[test]
@ -310,26 +257,20 @@ mod test_valid {
let template = Template::parse(r"日本\{アナキスト\}連盟").unwrap(); let template = Template::parse(r"日本\{アナキスト\}連盟").unwrap();
assert_eq!( assert_eq!(
template, template,
template!( template!("日本", r"{", "アナキスト", r"}", "連盟")
Text("日本"),
Text(r"{"),
Text("アナキスト"),
Text(r"}"),
Text("連盟")
)
); );
} }
#[test] #[test]
fn multibyte_rtl_text() { fn multibyte_rtl_text() {
let template = Template::parse("محمد صايل").unwrap(); let template = Template::parse("محمد صايل").unwrap();
assert_eq!(template, template!(Text("محمد صايل"))); assert_eq!(template, template!("محمد صايل"));
} }
#[test] #[test]
fn multibyte_rtl_key() { fn multibyte_rtl_key() {
let template = Template::parse("محمد {ريشة}").unwrap(); let template = Template::parse("محمد {ريشة}").unwrap();
assert_eq!(template, template!(Text("محمد "), Key("ريشة"))); assert_eq!(template, template!("محمد ", { "ريشة" }));
} }
} }

View file

@ -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}; use crate::{ParseError, RenderError, Values};
@ -138,17 +138,20 @@ impl<'s> Template<'s> {
Ok(String::from_utf8(buf).unwrap()) Ok(String::from_utf8(buf).unwrap())
} }
/// If the template contains key `key`.
pub fn has_key(&self, key: &str) -> bool { 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 { self.items.iter().any(|token| match token {
Item::Key(k) => keys.contains(k), Item::Key(k) => keys.contains(k),
_ => false, _ => false,
}) })
} }
/// Returns all keys in this template.
pub fn keys(&self) -> impl Iterator<Item = &&str> { pub fn keys(&self) -> impl Iterator<Item = &&str> {
self.items.iter().filter_map(|token| match token { self.items.iter().filter_map(|token| match token {
Item::Key(k) => Some(k), Item::Key(k) => Some(k),
@ -160,39 +163,116 @@ impl<'s> Template<'s> {
pub fn set_default(&mut self, default: &dyn Display) { pub fn set_default(&mut self, default: &dyn Display) {
self.default = Some(Cow::Owned(default.to_string())); 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> { impl<'s, 'rhs: 's> ops::AddAssign<&Template<'rhs>> for Template<'s> {
type Output = Self; fn add_assign(&mut self, rhs: &Template<'rhs>) {
fn add(mut self, rhs: Self) -> Self::Output {
self.items self.items
.to_mut() .to_mut()
.extend(rhs.items.as_ref().iter().cloned()); .extend(rhs.items.as_ref().iter().cloned());
if let Some(default) = &rhs.default {
self.default = Some(default.clone());
}
}
}
impl<'s, 'rhs: 's> ops::AddAssign<Template<'rhs>> 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 { if let Some(default) = rhs.default {
self.default = Some(default); self.default = Some(default);
} }
}
}
impl<'s, 'item: 's> ops::AddAssign<Item<'item>> 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<Template<'rhs>> 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<Item<'item>> 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 self
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::Item::{Key, Text}; use crate::Template;
#[test] #[test]
fn concat_templates() { fn concat_templates() {
let t1 = crate::template!(Text("Hello"), Key("name")); let t1 = crate::template!("Hello", { "name" });
let t2 = crate::template!(Text("have a"), Key("adjective"), Text("day")); let t2 = crate::template!("have a", { "adjective" }, "day");
assert_eq!( assert_eq!(
t1 + t2, t1 + t2,
crate::template!( crate::template!("Hello", { "name" }, "have a", { "adjective" }, "day"),
Text("Hello"),
Key("name"),
Text("have a"),
Key("adjective"),
Text("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());
}
} }

View file

@ -5,14 +5,14 @@ use std::{
}; };
pub trait Values { pub trait Values {
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>>; fn get_value(&self, key: &str) -> Option<Cow<'_, str>>;
} }
impl<T> Values for &T impl<T> Values for &T
where where
T: Values, T: Values,
{ {
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> { fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
T::get_value(self, key) T::get_value(self, key)
} }
} }
@ -22,7 +22,7 @@ where
K: AsRef<str>, K: AsRef<str>,
V: AsRef<str>, V: AsRef<str>,
{ {
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> { fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
self.iter().find_map(|(k, v)| { self.iter().find_map(|(k, v)| {
if k.as_ref() == key { if k.as_ref() == key {
Some(Cow::Borrowed(v.as_ref())) Some(Cow::Borrowed(v.as_ref()))
@ -38,7 +38,7 @@ where
K: AsRef<str>, K: AsRef<str>,
V: AsRef<str>, V: AsRef<str>,
{ {
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> { fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
(*self).get_value(key) (*self).get_value(key)
} }
} }
@ -48,7 +48,7 @@ where
K: AsRef<str>, K: AsRef<str>,
V: AsRef<str>, V: AsRef<str>,
{ {
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> { fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
self.as_slice().get_value(key) self.as_slice().get_value(key)
} }
} }
@ -58,7 +58,7 @@ where
K: AsRef<str>, K: AsRef<str>,
V: AsRef<str>, V: AsRef<str>,
{ {
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> { fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
self.as_slice().get_value(key) self.as_slice().get_value(key)
} }
} }
@ -69,7 +69,7 @@ where
V: AsRef<str>, V: AsRef<str>,
S: BuildHasher, S: BuildHasher,
{ {
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> { fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
self.get(key).map(|v| Cow::Borrowed(v.as_ref())) self.get(key).map(|v| Cow::Borrowed(v.as_ref()))
} }
} }
@ -79,7 +79,7 @@ where
K: Borrow<str> + Ord, K: Borrow<str> + Ord,
V: AsRef<str>, V: AsRef<str>,
{ {
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> { fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
self.get(key).map(|v| Cow::Borrowed(v.as_ref())) self.get(key).map(|v| Cow::Borrowed(v.as_ref()))
} }
} }
@ -87,25 +87,22 @@ where
/// Workaround to allow using functions as [`Values`]. /// Workaround to allow using functions as [`Values`].
/// ///
/// As this isn't constructible you'll want to use [`vals()`] instead. /// As this isn't constructible you'll want to use [`vals()`] instead.
pub struct ValuesFn<F> pub struct ValuesFn<F> {
where
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
{
inner: F, inner: F,
} }
impl<F> Values for ValuesFn<F> impl<'s, F> Values for &'s ValuesFn<F>
where where
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static, F: Fn(&str) -> Option<Cow<'s, str>> + 's,
{ {
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> { fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
(self.inner)(key) (self.inner)(key)
} }
} }
impl<F> From<F> for ValuesFn<F> impl<'f, F> From<F> for ValuesFn<F>
where where
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static, F: Fn(&str) -> Option<Cow<'f, str>> + 'f,
{ {
fn from(inner: F) -> Self { fn from(inner: F) -> Self {
Self { inner } Self { inner }
@ -123,11 +120,11 @@ where
/// ///
/// fn use_values(_values: impl Values) {} /// fn use_values(_values: impl Values) {}
/// ///
/// use_values(vals(|_| Some("hello".into()))); /// use_values(&vals(|_| Some("hello".into())));
/// ``` /// ```
pub const fn vals<F>(func: F) -> ValuesFn<F> pub const fn vals<'f, F>(func: F) -> ValuesFn<F>
where where
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static, F: Fn(&str) -> Option<Cow<'f, str>> + 'f,
{ {
ValuesFn { inner: func } ValuesFn { inner: func }
} }