use crate::target_error::TargetError;
use crate::target_scope::TargetScope;
use moon_common::{Id, ID_CHARS};
use once_cell::sync::Lazy;
use regex::Regex;
use schematic::{Schema, SchemaBuilder, Schematic};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::{
cmp::Ordering,
fmt::{self, Display},
};
use tracing::instrument;
pub static TARGET_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(&format!(
r"^(?P<scope>(?:[A-Za-z@#_]{{1}}{chars}|\^|~))?:(?P<task>{chars})$",
chars = ID_CHARS
))
.unwrap()
});
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Target {
pub id: String,
pub scope: TargetScope,
pub task_id: Id,
}
impl Target {
pub fn new<S, T>(scope_id: S, task_id: T) -> miette::Result<Target>
where
S: AsRef<str>,
T: AsRef<str>,
{
let scope_id = scope_id.as_ref();
let task_id = task_id.as_ref();
let handle_error = |_| TargetError::InvalidFormat(format!("{scope_id}:{task_id}"));
let scope = TargetScope::Project(Id::new(scope_id).map_err(handle_error)?);
Ok(Target {
id: Target::format(&scope, task_id),
scope,
task_id: Id::new(task_id).map_err(handle_error)?,
})
}
pub fn new_self<T>(task_id: T) -> miette::Result<Target>
where
T: AsRef<str>,
{
let task_id = task_id.as_ref();
Ok(Target {
id: Target::format(TargetScope::OwnSelf, task_id),
scope: TargetScope::OwnSelf,
task_id: Id::new(task_id)
.map_err(|_| TargetError::InvalidFormat(format!("~:{task_id}")))?,
})
}
pub fn format<S, T>(scope: S, task: T) -> String
where
S: AsRef<TargetScope>,
T: AsRef<str>,
{
format!("{}:{}", scope.as_ref(), task.as_ref())
}
#[instrument(name = "parse_target")]
pub fn parse(target_id: &str) -> miette::Result<Target> {
if target_id == ":" {
return Err(TargetError::TooWild.into());
}
if !target_id.contains(':') {
return Target::new_self(target_id);
}
let Some(matches) = TARGET_PATTERN.captures(target_id) else {
return Err(TargetError::InvalidFormat(target_id.to_owned()).into());
};
let scope = match matches.name("scope") {
Some(value) => match value.as_str() {
"" => TargetScope::All,
"^" => TargetScope::Deps,
"~" => TargetScope::OwnSelf,
id => {
if let Some(tag) = id.strip_prefix('#') {
TargetScope::Tag(Id::raw(tag))
} else {
TargetScope::Project(Id::raw(id))
}
}
},
None => TargetScope::All,
};
let task_id = Id::new(matches.name("task").unwrap().as_str())
.map_err(|_| TargetError::InvalidFormat(target_id.to_owned()))?;
Ok(Target {
id: target_id.to_owned(),
scope,
task_id,
})
}
pub fn as_str(&self) -> &str {
&self.id
}
pub fn is_all_task(&self, task_id: &str) -> bool {
if matches!(&self.scope, TargetScope::All) {
return if let Some(id) = task_id.strip_prefix(':') {
self.task_id == id
} else {
self.task_id == task_id
};
}
false
}
pub fn get_project_id(&self) -> Option<&Id> {
match &self.scope {
TargetScope::Project(id) => Some(id),
_ => None,
}
}
pub fn get_tag_id(&self) -> Option<&Id> {
match &self.scope {
TargetScope::Tag(id) => Some(id),
_ => None,
}
}
}
impl Default for Target {
fn default() -> Self {
Target {
id: "~:unknown".into(),
scope: TargetScope::OwnSelf,
task_id: Id::raw("unknown"),
}
}
}
impl Display for Target {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.id)
}
}
impl AsRef<Target> for Target {
fn as_ref(&self) -> &Target {
self
}
}
impl AsRef<str> for Target {
fn as_ref(&self) -> &str {
&self.id
}
}
impl PartialOrd for Target {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Target {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
impl<'de> Deserialize<'de> for Target {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Target::parse(&String::deserialize(deserializer)?).map_err(de::Error::custom)
}
}
impl Serialize for Target {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.id)
}
}
impl Schematic for Target {
fn build_schema(mut schema: SchemaBuilder) -> Schema {
schema.string_default()
}
}
impl From<&str> for Target {
fn from(value: &str) -> Self {
Target::parse(value).unwrap()
}
}