moon_config/shapes/
output_path.rs1#![allow(clippy::from_over_into)]
2
3use crate::patterns;
4use crate::portable_path::is_glob_like;
5use crate::validate::validate_child_relative_path;
6use moon_common::path::{
7 RelativeFrom, WorkspaceRelativePathBuf, expand_to_workspace_relative, standardize_separators,
8};
9use schematic::{ParseError, Schema, SchemaBuilder, Schematic, derive_enum};
10use std::cmp::Ordering;
11use std::str::FromStr;
12
13derive_enum!(
14 #[serde(untagged, into = "String", try_from = "String")]
16 pub enum OutputPath {
17 ProjectFile(String),
18 ProjectGlob(String),
19 TokenFunc(String),
20 TokenVar(String),
21 WorkspaceFile(String),
22 WorkspaceGlob(String),
23 }
24);
25
26impl OutputPath {
27 pub fn as_str(&self) -> &str {
28 match self {
29 Self::ProjectFile(value)
30 | Self::ProjectGlob(value)
31 | Self::TokenFunc(value)
32 | Self::TokenVar(value)
33 | Self::WorkspaceFile(value)
34 | Self::WorkspaceGlob(value) => value,
35 }
36 }
37
38 pub fn is_glob(&self) -> bool {
39 matches!(self, Self::ProjectGlob(_) | Self::WorkspaceGlob(_))
40 }
41
42 pub fn to_workspace_relative(
43 &self,
44 project_source: impl AsRef<str>,
45 ) -> Option<WorkspaceRelativePathBuf> {
46 match self {
47 Self::ProjectFile(path) | Self::ProjectGlob(path) => Some(
48 expand_to_workspace_relative(RelativeFrom::Project(project_source.as_ref()), path),
49 ),
50 Self::WorkspaceFile(path) | Self::WorkspaceGlob(path) => {
51 Some(expand_to_workspace_relative(RelativeFrom::Workspace, path))
52 }
53 _ => None,
54 }
55 }
56}
57
58impl AsRef<str> for OutputPath {
59 fn as_ref(&self) -> &str {
60 self.as_str()
61 }
62}
63
64impl AsRef<OutputPath> for OutputPath {
65 fn as_ref(&self) -> &OutputPath {
66 self
67 }
68}
69
70impl PartialOrd<OutputPath> for OutputPath {
71 fn partial_cmp(&self, other: &OutputPath) -> Option<Ordering> {
72 Some(self.cmp(other))
73 }
74}
75
76impl Ord for OutputPath {
77 fn cmp(&self, other: &Self) -> Ordering {
78 self.as_str().cmp(other.as_str())
79 }
80}
81
82impl FromStr for OutputPath {
83 type Err = ParseError;
84
85 fn from_str(value: &str) -> Result<Self, Self::Err> {
86 if value.starts_with('@') && patterns::TOKEN_FUNC_DISTINCT.is_match(value) {
88 return Ok(Self::TokenFunc(value.to_owned()));
89 }
90
91 if value.starts_with('$') {
93 if patterns::ENV_VAR_DISTINCT.is_match(value) {
94 return Err(ParseError::new(
95 "environment variable is not supported by itself",
96 ));
97 } else if patterns::ENV_VAR_GLOB_DISTINCT.is_match(value) {
98 return Err(ParseError::new(
99 "environment variable globs are not supported",
100 ));
101 } else if patterns::TOKEN_VAR_DISTINCT.is_match(value) {
102 return Ok(Self::TokenVar(value.to_owned()));
103 }
104 }
105
106 let value = standardize_separators(value);
107
108 if value.starts_with("/!") || value.starts_with("!/") {
110 return Ok(Self::WorkspaceGlob(format!("!{}", &value[2..])));
111 }
112
113 if let Some(workspace_path) = value.strip_prefix('/') {
115 validate_child_relative_path(workspace_path)
116 .map_err(|error| ParseError::new(error.to_string()))?;
117
118 return Ok(if is_glob_like(workspace_path) {
119 Self::WorkspaceGlob(workspace_path.to_owned())
120 } else {
121 Self::WorkspaceFile(workspace_path.to_owned())
122 });
123 }
124
125 validate_child_relative_path(&value).map_err(|error| ParseError::new(error.to_string()))?;
127
128 let project_path = value.trim_start_matches("./");
129
130 Ok(if is_glob_like(project_path) {
131 Self::ProjectGlob(project_path.to_owned())
132 } else {
133 Self::ProjectFile(project_path.to_owned())
134 })
135 }
136}
137
138impl TryFrom<String> for OutputPath {
139 type Error = ParseError;
140
141 fn try_from(value: String) -> Result<Self, Self::Error> {
142 Self::from_str(&value)
143 }
144}
145
146impl Into<String> for OutputPath {
147 fn into(self) -> String {
148 match self {
149 Self::ProjectFile(value)
150 | Self::ProjectGlob(value)
151 | Self::TokenFunc(value)
152 | Self::TokenVar(value) => value,
153 Self::WorkspaceFile(path) | Self::WorkspaceGlob(path) => format!("/{path}"),
154 }
155 }
156}
157
158impl Schematic for OutputPath {
159 fn build_schema(mut schema: SchemaBuilder) -> Schema {
160 schema.string_default()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn parses_correctly() {
170 assert_eq!(
172 OutputPath::from_str("file.rs").unwrap(),
173 OutputPath::ProjectFile("file.rs".into())
174 );
175 assert_eq!(
176 OutputPath::from_str("dir/file.rs").unwrap(),
177 OutputPath::ProjectFile("dir/file.rs".into())
178 );
179 assert_eq!(
180 OutputPath::from_str("dir/**/*").unwrap(),
181 OutputPath::ProjectGlob("dir/**/*".into())
182 );
183 assert_eq!(
184 OutputPath::from_str("!dir/**/*").unwrap(),
185 OutputPath::ProjectGlob("!dir/**/*".into())
186 );
187 assert_eq!(
188 OutputPath::from_str("./file.rs").unwrap(),
189 OutputPath::ProjectFile("file.rs".into())
190 );
191 assert_eq!(
192 OutputPath::from_str("./dir/file.rs").unwrap(),
193 OutputPath::ProjectFile("dir/file.rs".into())
194 );
195 assert_eq!(
196 OutputPath::from_str("././dir/**/*").unwrap(),
197 OutputPath::ProjectGlob("dir/**/*".into())
198 );
199
200 assert_eq!(
202 OutputPath::from_str("/file.rs").unwrap(),
203 OutputPath::WorkspaceFile("file.rs".into())
204 );
205 assert_eq!(
206 OutputPath::from_str("/dir/file.rs").unwrap(),
207 OutputPath::WorkspaceFile("dir/file.rs".into())
208 );
209 assert_eq!(
210 OutputPath::from_str("/dir/**/*").unwrap(),
211 OutputPath::WorkspaceGlob("dir/**/*".into())
212 );
213 assert_eq!(
214 OutputPath::from_str("!/dir/**/*").unwrap(),
215 OutputPath::WorkspaceGlob("!dir/**/*".into())
216 );
217 assert_eq!(
218 OutputPath::from_str("/!dir/**/*").unwrap(),
219 OutputPath::WorkspaceGlob("!dir/**/*".into())
220 );
221 }
222
223 #[test]
224 fn parses_tokens() {
225 assert_eq!(
227 OutputPath::from_str("@group(name)").unwrap(),
228 OutputPath::TokenFunc("@group(name)".into())
229 );
230 assert_eq!(
231 OutputPath::from_str("@dirs(name)").unwrap(),
232 OutputPath::TokenFunc("@dirs(name)".into())
233 );
234 assert_eq!(
235 OutputPath::from_str("@files(name)").unwrap(),
236 OutputPath::TokenFunc("@files(name)".into())
237 );
238 assert_eq!(
239 OutputPath::from_str("@globs(name)").unwrap(),
240 OutputPath::TokenFunc("@globs(name)".into())
241 );
242 assert_eq!(
243 OutputPath::from_str("@root(name)").unwrap(),
244 OutputPath::TokenFunc("@root(name)".into())
245 );
246
247 assert_eq!(
249 OutputPath::from_str("$workspaceRoot").unwrap(),
250 OutputPath::TokenVar("$workspaceRoot".into())
251 );
252 assert_eq!(
253 OutputPath::from_str("$projectType").unwrap(),
254 OutputPath::TokenVar("$projectType".into())
255 );
256 }
257
258 #[test]
259 #[should_panic(expected = "environment variable globs are not supported")]
260 fn errors_for_env_globs() {
261 OutputPath::from_str("$VAR_*").unwrap();
262 }
263
264 #[test]
265 #[should_panic(expected = "parent relative paths are not supported")]
266 fn errors_for_parent_relative_from_project() {
267 OutputPath::from_str("../test").unwrap();
268 }
269
270 #[test]
271 #[should_panic(expected = "parent relative paths are not supported")]
272 fn errors_for_parent_relative_from_workspace() {
273 OutputPath::from_str("/../test").unwrap();
274 }
275}