moon_target/
target.rs

1use crate::target_error::TargetError;
2use crate::target_scope::TargetScope;
3use compact_str::CompactString;
4use moon_common::{ID_CHARS, ID_SYMBOLS, Id, Style, Stylize, color};
5use regex::Regex;
6use schematic::{Schema, SchemaBuilder, Schematic};
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
8use std::sync::LazyLock;
9use std::{cmp::Ordering, fmt};
10use tracing::instrument;
11
12// The @ is to support npm package scopes!
13pub static TARGET_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
14    Regex::new(&format!(
15        r"^(?P<scope>(?:[A-Za-z@#_]{{1}}[{ID_CHARS}{ID_SYMBOLS}]*|\^|~))?:(?P<task>[{ID_CHARS}{ID_SYMBOLS}]+)$"
16    ))
17    .unwrap()
18});
19
20#[derive(Clone, Eq, Hash, PartialEq)]
21pub struct Target {
22    pub id: CompactString,
23    pub scope: TargetScope,
24    pub task_id: Id,
25}
26
27impl Target {
28    pub fn new<S, T>(scope_id: S, task_id: T) -> miette::Result<Target>
29    where
30        S: AsRef<str>,
31        T: AsRef<str>,
32    {
33        let scope_id = scope_id.as_ref();
34        let task_id = task_id.as_ref();
35
36        let handle_error = |_| TargetError::InvalidFormat(format!("{scope_id}:{task_id}"));
37        let scope = TargetScope::Project(Id::new(scope_id).map_err(handle_error)?);
38
39        Ok(Target {
40            id: CompactString::new(Target::format(&scope, task_id)),
41            scope,
42            task_id: Id::new(task_id).map_err(handle_error)?,
43        })
44    }
45
46    pub fn new_self<T>(task_id: T) -> miette::Result<Target>
47    where
48        T: AsRef<str>,
49    {
50        let task_id = task_id.as_ref();
51
52        Ok(Target {
53            id: CompactString::new(Target::format(TargetScope::OwnSelf, task_id)),
54            scope: TargetScope::OwnSelf,
55            task_id: Id::new(task_id)
56                .map_err(|_| TargetError::InvalidFormat(format!("~:{task_id}")))?,
57        })
58    }
59
60    pub fn format<S, T>(scope: S, task: T) -> String
61    where
62        S: AsRef<TargetScope>,
63        T: AsRef<str>,
64    {
65        format!("{}:{}", scope.as_ref(), task.as_ref())
66    }
67
68    #[instrument(name = "parse_target")]
69    pub fn parse(target_id: &str) -> miette::Result<Target> {
70        if target_id == ":" {
71            return Err(TargetError::TooWild.into());
72        }
73
74        if !target_id.contains(':') {
75            return Target::new_self(target_id);
76        }
77
78        let Some(matches) = TARGET_PATTERN.captures(target_id) else {
79            return Err(TargetError::InvalidFormat(target_id.to_owned()).into());
80        };
81
82        let scope = match matches.name("scope") {
83            Some(value) => match value.as_str() {
84                "" => TargetScope::All,
85                "^" => TargetScope::Deps,
86                "~" => TargetScope::OwnSelf,
87                id => {
88                    if let Some(tag) = id.strip_prefix('#') {
89                        TargetScope::Tag(Id::raw(tag))
90                    } else {
91                        TargetScope::Project(Id::raw(id))
92                    }
93                }
94            },
95            None => TargetScope::All,
96        };
97
98        let task_id = Id::new(matches.name("task").unwrap().as_str())
99            .map_err(|_| TargetError::InvalidFormat(target_id.to_owned()))?;
100
101        Ok(Target {
102            id: CompactString::new(target_id),
103            scope,
104            task_id,
105        })
106    }
107
108    pub fn parse_strict(target_id: &str) -> miette::Result<Target> {
109        if !target_id.contains(':') {
110            return Err(TargetError::ProjectScopeRequired(target_id.into()).into());
111        }
112
113        Self::parse(target_id)
114    }
115
116    pub fn as_str(&self) -> &str {
117        &self.id
118    }
119
120    pub fn to_prefix(&self, width: Option<usize>) -> String {
121        let prefix = self.as_str();
122
123        let label = if let Some(width) = width {
124            format!("{prefix: >width$}")
125        } else {
126            prefix.to_owned()
127        };
128
129        if color::no_color() {
130            format!("{label} | ")
131        } else {
132            format!("{} {} ", color::log_target(label), color::muted("|"))
133        }
134    }
135
136    pub fn is_all_task(&self, task_id: &str) -> bool {
137        if matches!(&self.scope, TargetScope::All) {
138            return if let Some(id) = task_id.strip_prefix(':') {
139                self.task_id == id
140            } else {
141                self.task_id == task_id
142            };
143        }
144
145        false
146    }
147
148    pub fn get_project_id(&self) -> miette::Result<&Id> {
149        match &self.scope {
150            TargetScope::Project(id) => Ok(id),
151            _ => Err(TargetError::ProjectScopeRequired(self.id.to_string()).into()),
152        }
153    }
154
155    pub fn get_tag_id(&self) -> Option<&Id> {
156        match &self.scope {
157            TargetScope::Tag(id) => Some(id),
158            _ => None,
159        }
160    }
161}
162
163impl Default for Target {
164    fn default() -> Self {
165        Target {
166            id: "~:unknown".into(),
167            scope: TargetScope::OwnSelf,
168            task_id: Id::raw("unknown"),
169        }
170    }
171}
172
173impl fmt::Debug for Target {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        write!(f, "{}", self.id)
176    }
177}
178
179impl fmt::Display for Target {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(f, "{}", self.id)
182    }
183}
184
185impl Stylize for Target {
186    fn style(&self, style: Style) -> String {
187        self.to_string().style(style)
188    }
189}
190
191impl AsRef<Target> for Target {
192    fn as_ref(&self) -> &Target {
193        self
194    }
195}
196
197impl AsRef<str> for Target {
198    fn as_ref(&self) -> &str {
199        &self.id
200    }
201}
202
203impl PartialOrd for Target {
204    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
205        Some(self.cmp(other))
206    }
207}
208
209impl Ord for Target {
210    fn cmp(&self, other: &Self) -> Ordering {
211        self.id.cmp(&other.id)
212    }
213}
214
215impl<'de> Deserialize<'de> for Target {
216    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
217    where
218        D: Deserializer<'de>,
219    {
220        Target::parse(&String::deserialize(deserializer)?).map_err(de::Error::custom)
221    }
222}
223
224impl Serialize for Target {
225    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
226    where
227        S: Serializer,
228    {
229        serializer.serialize_str(&self.id)
230    }
231}
232
233impl Schematic for Target {
234    fn build_schema(mut schema: SchemaBuilder) -> Schema {
235        schema.string_default()
236    }
237}
238
239// This is only used by tests!
240
241impl From<&str> for Target {
242    fn from(value: &str) -> Self {
243        Target::parse(value).unwrap()
244    }
245}