blob: 5d3fbe9228adf93b78469ede530e2c3b8daf21dd [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Extracts the fields of a GN label. GN labels are meant to be opaque in
//! output, and GN itself provides helper functions that does the same tasks
//! that are here.
use std::{fmt::Display, str::FromStr};
use thiserror::Error;
// The TargetLabel is capable of representing a complete "canonical" GN label.
//
// Use its methods to access it's various representations.
//
#[derive(Debug, Clone, PartialEq)]
pub struct TargetLabel {
label: Label,
toolchain: Label,
}
impl TargetLabel {
/// Equivalent to GN's `get_label_info(label, "name")`
pub fn name(&self) -> &str {
&self.label.name
}
/// Equivalent to GN's `get_label_info(label, "dir")`
pub fn dir(&self) -> &str {
&self.label.path
}
pub fn label_no_toolchain(&self) -> String {
self.label.to_string()
}
pub fn label_with_toolchain(&self) -> String {
// defer to Display impl
self.to_string()
}
}
impl Display for TargetLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}({})", self.label, self.toolchain))
}
}
#[derive(Error, Debug)]
pub enum LabelParseErrors {
#[error("A label without toolchain cannot have a toolchain: {0}")]
LabelHasToolchainWhenItShouldNot(String),
#[error("Label is missing a path: {0}")]
PathMissing(String),
#[error("Label is missing a name separator: {0}")]
NameSeparatorMissing(String),
#[error("Label is missing a name: {0}")]
NameMissing(String),
#[error("No closing parens on toolchain: {0}")]
InvalidToolchain(String),
#[error("Label doesn't have a toolchain: {0}")]
TargetLabelMissingToolchain(String),
}
impl FromStr for TargetLabel {
type Err = LabelParseErrors;
/// Parses the string as a canonical, fully-qualified, label, returning an
/// error, otherwise.
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(string) = s.strip_suffix(")") {
if let Some((label, toolchain)) = string.split_once("(") {
let label = label.try_into()?;
let toolchain = toolchain.try_into()?;
Ok(Self { label, toolchain })
} else {
Err(LabelParseErrors::InvalidToolchain(s.to_owned()))
}
} else {
Err(LabelParseErrors::TargetLabelMissingToolchain(s.to_owned()))
}
}
}
impl TryFrom<&str> for TargetLabel {
type Error = LabelParseErrors;
fn try_from(s: &str) -> Result<Self, Self::Error> {
s.parse()
}
}
#[derive(Debug, Clone, PartialEq)]
struct Label {
path: String,
name: String,
}
impl TryFrom<&str> for Label {
type Error = LabelParseErrors;
fn try_from(string: &str) -> Result<Self, Self::Error> {
if string.contains("(") {
return Err(LabelParseErrors::LabelHasToolchainWhenItShouldNot(string.to_owned()));
}
if let Some((path, name)) = string.split_once(":") {
if path.len() == 0 {
Err(LabelParseErrors::PathMissing(string.to_owned()))
} else if name.len() == 0 {
Err(LabelParseErrors::NameMissing(string.to_owned()))
} else {
Ok(Self { path: path.to_string(), name: name.to_string() })
}
} else {
Err(LabelParseErrors::NameSeparatorMissing(string.to_owned()))
}
}
}
impl std::fmt::Display for Label {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.path, self.name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_label_display() {
let label = Label { path: "//some/path".into(), name: "name".into() };
assert_eq!(format!("{}", label), "//some/path:name");
}
#[test]
fn test_targetlabel_display() {
let target_label = TargetLabel {
label: Label { path: "//some/other/path".into(), name: "name".into() },
toolchain: Label { path: "//toolchain/path".into(), name: "toolchain_name".into() },
};
assert_eq!(
format!("{}", target_label),
"//some/other/path:name(//toolchain/path:toolchain_name)"
);
assert_eq!(
target_label.label_with_toolchain(),
"//some/other/path:name(//toolchain/path:toolchain_name)"
);
assert_eq!(target_label.label_no_toolchain(), "//some/other/path:name")
}
#[test]
fn test_valid_canonical_label() {
let string = "//path/to/file:name";
let label: Label = string.try_into().unwrap();
assert_eq!(label.path, "//path/to/file");
assert_eq!(label.name, "name");
}
#[test]
fn test_valid_canonical_target_label() {
let string = "//path/to/file:name(//diff/path:toolchain)";
let label: TargetLabel = string.try_into().unwrap();
assert_eq!(label.dir(), "//path/to/file");
assert_eq!(label.name(), "name");
assert_eq!(&label.label_no_toolchain(), "//path/to/file:name");
assert_eq!(&label.label_with_toolchain(), string);
}
#[test]
fn test_reject_missing_paren() {
let string = "//path/to/file:name(//diff/path:toolchain";
let result = TargetLabel::try_from(string);
assert!(result.is_err());
}
#[test]
fn test_reject_missing_toolchain() {
let string = "//path/to/file:name";
let result = TargetLabel::try_from(string);
assert!(result.is_err());
}
#[test]
fn test_reject_missing_label_name() {
let string = "//path/to/file(//diff/path:toolchain)";
let result = TargetLabel::try_from(string);
assert!(result.is_err());
}
#[test]
fn test_reject_missing_label_dir() {
let string = ":file(//diff/path:toolchain)";
let result = TargetLabel::try_from(string);
assert!(result.is_err());
}
#[test]
fn test_reject_missing_toolchain_name() {
let string = "//path/to/file:name(//diff/path)";
let result = TargetLabel::try_from(string);
assert!(result.is_err());
}
#[test]
fn test_reject_missing_toolchain_dir() {
let string = "//path/to/file:name(toolchain)";
let result = TargetLabel::try_from(string);
assert!(result.is_err());
}
#[test]
fn test_reject_nested_toolchain() {
let string = "//path/to/file:name(//diff/path:toolchain(//diff/path:toolchain))";
let result = TargetLabel::try_from(string);
assert!(result.is_err());
}
}