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",
"itertools",
"jobslot",
"leon",
"maybe-owned",
"miette",
"normalize-path",
@ -178,7 +179,6 @@ dependencies = [
"strum",
"tempfile",
"thiserror",
"tinytemplate",
"tokio",
"tracing",
"url",

View file

@ -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"],
}
}

View file

@ -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"

View file

@ -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<Self, BinstallError> {
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<String, BinstallError> {
Ok(tt.render("bin_dir", self)?)
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)),
"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 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<TinyTemplateError>),
#[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<RemoteError> for BinstallError {
}
}
impl From<TinyTemplateError> for BinstallError {
fn from(e: TinyTemplateError) -> Self {
BinstallError::Template(Box::new(e))
}
}
impl From<CargoTomlError> for BinstallError {
fn from(e: CargoTomlError) -> Self {
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 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<Option<(Url, PkgFmt)>, 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<Item = impl Future<Output = FindTaskRes> + '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) {
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<ExtractedFiles, BinstallError> {
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<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> {
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() {
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..]
};
}
});
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<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)]
pub(self) fn render_url(&self, template: &str) -> Result<Url, BinstallError> {
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")
);
}

View file

@ -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<impl Iterator<Item = String> + Clone + 'static> {
) -> Option<impl Iterator<Item = Template<'static>> + 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<Item = String> + Clone + 'static {
) -> impl Iterator<Item = Template<'static>> + 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
})
}

View file

@ -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::<Result<Vec<_>, BinstallError>>()?;
let mut source_set = BTreeSet::new();

View file

@ -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<u8> = 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<Cow<'s, str>> {
//! fn get_value(&self, key: &str) -> Option<Cow<'_, str>> {
//! if key == "name" {
//! Some(self.name.into())
//! } 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:
///
@ -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),*];
() => {
$crate::Template::new(
{
const ITEMS: &'static [$crate::Item<'static>] = &[];
ITEMS
}, ::core::option::Option::None)
},
::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
)
);
}
}

View file

@ -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!("محمد ", { "ريشة" }));
}
}

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};
@ -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<Item = &&str> {
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<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 {
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
}
}
#[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());
}
}

View file

@ -5,14 +5,14 @@ use std::{
};
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
where
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)
}
}
@ -22,7 +22,7 @@ where
K: 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)| {
if k.as_ref() == key {
Some(Cow::Borrowed(v.as_ref()))
@ -38,7 +38,7 @@ where
K: 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)
}
}
@ -48,7 +48,7 @@ where
K: 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)
}
}
@ -58,7 +58,7 @@ where
K: 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)
}
}
@ -69,7 +69,7 @@ where
V: AsRef<str>,
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()))
}
}
@ -79,7 +79,7 @@ where
K: Borrow<str> + Ord,
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()))
}
}
@ -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<F>
where
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
{
pub struct ValuesFn<F> {
inner: F,
}
impl<F> Values for ValuesFn<F>
impl<'s, F> Values for &'s ValuesFn<F>
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)
}
}
impl<F> From<F> for ValuesFn<F>
impl<'f, F> From<F> for ValuesFn<F>
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 {
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<F>(func: F) -> ValuesFn<F>
pub const fn vals<'f, F>(func: F) -> ValuesFn<F>
where
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
F: Fn(&str) -> Option<Cow<'f, str>> + 'f,
{
ValuesFn { inner: func }
}