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
12pub 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
239impl From<&str> for Target {
242 fn from(value: &str) -> Self {
243 Target::parse(value).unwrap()
244 }
245}