use std::fmt::{self, Display};
use std::str::FromStr;
use anyhow::{bail, Context, Result};
use camino::Utf8Path;
use once_cell::sync::OnceCell;
use regex::Regex;
use serde::de::Visitor;
use serde::{Deserialize, Serialize, Serializer};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub(crate) enum Label {
Relative {
target: String,
},
Absolute {
repository: Repository,
package: String,
target: String,
},
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub(crate) enum Repository {
Canonical(String), Explicit(String), Local, }
impl Label {
#[cfg(test)]
pub(crate) fn is_absolute(&self) -> bool {
match self {
Label::Relative { .. } => false,
Label::Absolute { .. } => true,
}
}
#[cfg(test)]
pub(crate) fn repository(&self) -> Option<&Repository> {
match self {
Label::Relative { .. } => None,
Label::Absolute { repository, .. } => Some(repository),
}
}
pub(crate) fn package(&self) -> Option<&str> {
match self {
Label::Relative { .. } => None,
Label::Absolute { package, .. } => Some(package.as_str()),
}
}
pub(crate) fn target(&self) -> &str {
match self {
Label::Relative { target } => target.as_str(),
Label::Absolute { target, .. } => target.as_str(),
}
}
}
impl FromStr for Label {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: OnceCell<Regex> = OnceCell::new();
let re = RE.get_or_try_init(|| {
Regex::new(r"^(@@?[\w\d\-_\.+~]*)?(//)?([\w\d\-_\./+]+)?(:([\+\w\d\-_\./]+))?$")
});
let cap = re?
.captures(s)
.with_context(|| format!("Failed to parse label from string: {s}"))?;
let (repository, is_absolute) = match (cap.get(1), cap.get(2).is_some()) {
(Some(repository), is_absolute) => match *repository.as_str().as_bytes() {
[b'@', b'@', ..] => (
Some(Repository::Canonical(repository.as_str()[2..].to_owned())),
is_absolute,
),
[b'@', ..] => (
Some(Repository::Explicit(repository.as_str()[1..].to_owned())),
is_absolute,
),
_ => bail!("Invalid Label: {}", s),
},
(None, true) => (Some(Repository::Local), true),
(None, false) => (None, false),
};
let package = cap.get(3).map(|package| package.as_str().to_owned());
let target = cap.get(5).map(|target| target.as_str().to_owned());
match repository {
None => match (package, target) {
(None, Some(target)) => Ok(Label::Relative { target }),
(Some(package), None) => Ok(Label::Relative { target: package }),
(None, None) => bail!("Invalid Label: {}", s),
(Some(_), Some(_)) => bail!("Invalid Label: {}", s),
},
Some(repository) => match (is_absolute, package, target) {
(true, Some(package), Some(target)) => Ok(Label::Absolute {
repository,
package,
target,
}),
(_, None, None) => match &repository {
Repository::Canonical(target) | Repository::Explicit(target) => {
let target = match target.is_empty() {
false => target.clone(),
true => bail!("Invalid Label: {}", s),
};
Ok(Label::Absolute {
repository,
package: String::new(),
target,
})
}
Repository::Local => bail!("Invalid Label: {}", s),
},
(true, Some(package), None) => {
let target = Utf8Path::new(&package)
.file_name()
.with_context(|| format!("Invalid Label: {}", s))?
.to_owned();
Ok(Label::Absolute {
repository,
package,
target,
})
}
(true, None, Some(target)) => Ok(Label::Absolute {
repository,
package: String::new(),
target,
}),
(false, Some(_), Some(_)) => bail!("Invalid Label: {}", s),
(false, Some(_), None) => bail!("Invalid Label: {}", s),
(false, None, Some(_)) => bail!("Invalid Label: {}", s),
},
}
}
}
impl Display for Label {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Label::Relative { target } => write!(f, ":{}", target),
Label::Absolute {
repository,
package,
target,
} => match repository {
Repository::Canonical(repository) => {
write!(f, "@@{repository}//{package}:{target}")
}
Repository::Explicit(repository) => {
write!(f, "@{repository}//{package}:{target}")
}
Repository::Local => write!(f, "//{package}:{target}"),
},
}
}
}
impl Serialize for Label {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.repr())
}
}
struct LabelVisitor;
impl Visitor<'_> for LabelVisitor {
type Value = Label;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Expected string value of `{name} {version}`.")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Label::from_str(v).map_err(E::custom)
}
}
impl<'de> Deserialize<'de> for Label {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(LabelVisitor)
}
}
impl Label {
pub(crate) fn repr(&self) -> String {
self.to_string()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn relative() {
let label = Label::from_str(":target").unwrap();
assert_eq!(label.to_string(), ":target");
assert!(!label.is_absolute());
assert_eq!(label.repository(), None);
assert_eq!(label.package(), None);
assert_eq!(label.target(), "target");
}
#[test]
fn relative_implicit() {
let label = Label::from_str("target").unwrap();
assert_eq!(label.to_string(), ":target");
assert!(!label.is_absolute());
assert_eq!(label.repository(), None);
assert_eq!(label.package(), None);
assert_eq!(label.target(), "target");
}
#[test]
fn absolute_full() {
let label = Label::from_str("@repo//package:target").unwrap();
assert_eq!(label.to_string(), "@repo//package:target");
assert!(label.is_absolute());
assert_eq!(
label.repository(),
Some(&Repository::Explicit(String::from("repo")))
);
assert_eq!(label.package(), Some("package"));
assert_eq!(label.target(), "target");
}
#[test]
fn absolute_repository() {
let label = Label::from_str("@repo").unwrap();
assert_eq!(label.to_string(), "@repo//:repo");
assert!(label.is_absolute());
assert_eq!(
label.repository(),
Some(&Repository::Explicit(String::from("repo")))
);
assert_eq!(label.package(), Some(""));
assert_eq!(label.target(), "repo");
}
#[test]
fn absolute_package() {
let label = Label::from_str("//package").unwrap();
assert_eq!(label.to_string(), "//package:package");
assert!(label.is_absolute());
assert_eq!(label.repository(), Some(&Repository::Local));
assert_eq!(label.package(), Some("package"));
assert_eq!(label.target(), "package");
let label = Label::from_str("//package/subpackage").unwrap();
assert_eq!(label.to_string(), "//package/subpackage:subpackage");
assert!(label.is_absolute());
assert_eq!(label.repository(), Some(&Repository::Local));
assert_eq!(label.package(), Some("package/subpackage"));
assert_eq!(label.target(), "subpackage");
}
#[test]
fn absolute_target() {
let label = Label::from_str("//:target").unwrap();
assert_eq!(label.to_string(), "//:target");
assert!(label.is_absolute());
assert_eq!(label.repository(), Some(&Repository::Local));
assert_eq!(label.package(), Some(""));
assert_eq!(label.target(), "target");
}
#[test]
fn absolute_repository_package() {
let label = Label::from_str("@repo//package").unwrap();
assert_eq!(label.to_string(), "@repo//package:package");
assert!(label.is_absolute());
assert_eq!(
label.repository(),
Some(&Repository::Explicit(String::from("repo")))
);
assert_eq!(label.package(), Some("package"));
assert_eq!(label.target(), "package");
}
#[test]
fn absolute_repository_target() {
let label = Label::from_str("@repo//:target").unwrap();
assert_eq!(label.to_string(), "@repo//:target");
assert!(label.is_absolute());
assert_eq!(
label.repository(),
Some(&Repository::Explicit(String::from("repo")))
);
assert_eq!(label.package(), Some(""));
assert_eq!(label.target(), "target");
}
#[test]
fn absolute_package_target() {
let label = Label::from_str("//package:target").unwrap();
assert_eq!(label.to_string(), "//package:target");
assert!(label.is_absolute());
assert_eq!(label.repository(), Some(&Repository::Local));
assert_eq!(label.package(), Some("package"));
assert_eq!(label.target(), "target");
}
#[test]
fn invalid_empty() {
Label::from_str("").unwrap_err();
Label::from_str("@").unwrap_err();
Label::from_str("//").unwrap_err();
Label::from_str(":").unwrap_err();
}
#[test]
fn invalid_relative_repository_package_target() {
Label::from_str("@repo/package:target").unwrap_err();
}
#[test]
fn invalid_relative_repository_package() {
Label::from_str("@repo/package").unwrap_err();
}
#[test]
fn invalid_relative_repository_target() {
Label::from_str("@repo:target").unwrap_err();
}
#[test]
fn invalid_relative_package_target() {
Label::from_str("package:target").unwrap_err();
}
#[test]
fn full_label_bzlmod() {
let label = Label::from_str("@@repo//package/sub_package:target").unwrap();
assert_eq!(label.to_string(), "@@repo//package/sub_package:target");
assert!(label.is_absolute());
assert_eq!(
label.repository(),
Some(&Repository::Canonical(String::from("repo")))
);
assert_eq!(label.package(), Some("package/sub_package"));
assert_eq!(label.target(), "target");
}
#[test]
fn full_label_bzlmod_with_tilde() {
let label = Label::from_str("@@repo~name//package/sub_package:target").unwrap();
assert_eq!(label.to_string(), "@@repo~name//package/sub_package:target");
assert!(label.is_absolute());
assert_eq!(
label.repository(),
Some(&Repository::Canonical(String::from("repo~name")))
);
assert_eq!(label.package(), Some("package/sub_package"));
assert_eq!(label.target(), "target");
}
#[test]
fn full_label_with_slash_after_colon() {
let label = Label::from_str("@repo//package/sub_package:subdir/target").unwrap();
assert_eq!(
label.to_string(),
"@repo//package/sub_package:subdir/target"
);
assert!(label.is_absolute());
assert_eq!(
label.repository(),
Some(&Repository::Explicit(String::from("repo")))
);
assert_eq!(label.package(), Some("package/sub_package"));
assert_eq!(label.target(), "subdir/target");
}
#[test]
fn label_contains_plus() {
let label = Label::from_str("@repo//vendor/wasi-0.11.0+wasi-snapshot-preview1:BUILD.bazel")
.unwrap();
assert!(label.is_absolute());
assert_eq!(
label.repository(),
Some(&Repository::Explicit(String::from("repo")))
);
assert_eq!(
label.package(),
Some("vendor/wasi-0.11.0+wasi-snapshot-preview1")
);
assert_eq!(label.target(), "BUILD.bazel");
}
#[test]
fn invalid_double_colon() {
Label::from_str("::target").unwrap_err();
}
#[test]
fn invalid_triple_at() {
Label::from_str("@@@repo//pkg:target").unwrap_err();
}
}