1use super::portable_path::{FilePath, GlobPath, PortablePath, is_glob_like};
2use super::*;
3use crate::{
4 DependencyScope, config_struct, config_unit_enum, generate_io_file_methods,
5 generate_io_glob_methods, patterns,
6};
7use deserialize_untagged_verbose_error::DeserializeUntaggedVerboseError;
8use moon_common::Id;
9use moon_common::path::{
10 RelativeFrom, WorkspaceRelativePathBuf, expand_to_workspace_relative, standardize_separators,
11};
12use schematic::{
13 Config, ConfigEnum, ParseError, RegexSetting, Schema, SchemaBuilder, Schematic,
14 schema::UnionType,
15};
16use serde::{Deserialize, Serialize, Serializer};
17use std::str::FromStr;
18
19config_struct!(
20 #[derive(Config)]
22 pub struct FileInput {
23 pub file: FilePath,
25
26 #[serde(
29 default,
30 alias = "match",
31 alias = "matches",
32 skip_serializing_if = "Option::is_none"
33 )]
34 pub content: Option<RegexSetting>,
35
36 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub optional: Option<bool>,
40 }
41);
42
43generate_io_file_methods!(FileInput);
44
45impl FileInput {
46 pub fn from_uri(uri: Uri) -> Result<Self, ParseError> {
47 let mut input = Self {
48 file: FilePath::parse(&uri.path)?,
49 ..Default::default()
50 };
51
52 for (key, value) in uri.query {
53 match key.as_str() {
54 "content" | "match" | "matches" => {
55 if !value.is_empty() {
56 input.content = Some(RegexSetting::new(value).map_err(map_parse_error)?);
57 }
58 }
59 "optional" => {
60 input.optional = Some(parse_bool_field(&key, &value)?);
61 }
62 _ => {
63 return Err(ParseError::new(format!("unknown file field `{key}`")));
64 }
65 };
66 }
67
68 Ok(input)
69 }
70}
71
72config_unit_enum!(
73 #[derive(ConfigEnum)]
75 pub enum FileGroupInputFormat {
76 #[default]
78 Static,
79 Dirs,
81 Envs,
83 Files,
85 Globs,
87 Root,
89 }
90);
91
92config_struct!(
93 #[derive(Config)]
95 pub struct FileGroupInput {
96 pub group: Id,
98
99 #[serde(default, alias = "as")]
101 pub format: FileGroupInputFormat,
102 }
103);
104
105impl FileGroupInput {
106 pub fn from_uri(uri: Uri) -> Result<Self, ParseError> {
107 let mut input = Self {
108 group: if uri.path.is_empty() {
109 return Err(ParseError::new("a file group identifier is required"));
110 } else {
111 Id::new(&uri.path).map_err(map_parse_error)?
112 },
113 ..Default::default()
114 };
115
116 for (key, value) in uri.query {
117 match key.as_str() {
118 "as" | "format" => {
119 input.format =
120 FileGroupInputFormat::from_str(&value).map_err(map_parse_error)?
121 }
122 _ => {
123 return Err(ParseError::new(format!("unknown file group field `{key}`")));
124 }
125 };
126 }
127
128 Ok(input)
129 }
130}
131
132config_struct!(
133 #[derive(Config)]
135 pub struct GlobInput {
136 pub glob: GlobPath,
138
139 #[serde(default = "default_true", skip_serializing_if = "is_false")]
141 #[setting(default = true)]
142 pub cache: bool,
143 }
144);
145
146generate_io_glob_methods!(GlobInput);
147
148impl GlobInput {
149 pub fn from_uri(uri: Uri) -> Result<Self, ParseError> {
150 let mut input = Self {
151 glob: GlobPath::parse(uri.path.replace("__QM__", "?"))?,
152 ..Default::default()
153 };
154
155 for (key, value) in uri.query {
156 match key.as_str() {
157 "cache" => {
158 input.cache = parse_bool_field(&key, &value)?;
159 }
160 _ => {
161 return Err(ParseError::new(format!("unknown glob field `{key}`")));
162 }
163 };
164 }
165
166 Ok(input)
167 }
168}
169
170config_struct!(
171 #[derive(Config)]
173 pub struct ManifestDepsInput {
174 pub manifest: Id,
176
177 #[serde(
180 default,
181 alias = "dep",
182 alias = "dependencies",
183 skip_serializing_if = "Vec::is_empty"
184 )]
185 pub deps: Vec<String>,
186 }
187);
188
189impl ManifestDepsInput {
190 pub fn from_uri(uri: Uri) -> Result<Self, ParseError> {
191 let mut input = Self {
192 manifest: if uri.path.is_empty() {
193 return Err(ParseError::new("a toolchain identifier is required"));
194 } else {
195 Id::new(&uri.path).map_err(map_parse_error)?
196 },
197 ..Default::default()
198 };
199
200 for (key, value) in uri.query {
201 match key.as_str() {
202 "dep" | "deps" | "dependencies" => {
203 for val in value.split(',') {
204 if !val.is_empty() {
205 input.deps.push(val.trim().to_owned());
206 }
207 }
208 }
209 _ => {
210 return Err(ParseError::new(format!("unknown manifest field `{key}`")));
211 }
212 };
213 }
214
215 Ok(input)
216 }
217}
218
219config_struct!(
220 #[derive(Config)]
222 pub struct ProjectInput {
223 pub project: String,
226
227 #[serde(default, skip_serializing_if = "Vec::is_empty")]
230 pub filter: Vec<String>,
231
232 #[serde(default, alias = "fileGroup", skip_serializing_if = "Option::is_none")]
235 pub group: Option<Id>,
236 }
237);
238
239impl ProjectInput {
240 pub fn from_uri(uri: Uri) -> Result<Self, ParseError> {
241 let mut input = Self {
242 project: if uri.path.is_empty() {
243 return Err(ParseError::new("a project identifier is required"));
244 } else if uri.path.starts_with('^') {
245 uri.path
246 } else {
247 Id::new(&uri.path).map_err(map_parse_error)?.to_string()
248 },
249 ..Default::default()
250 };
251
252 for (key, value) in uri.query {
253 match key.as_str() {
254 "filter" => {
255 if !value.is_empty() {
256 input.filter.push(value);
257 }
258 }
259 "fileGroup" | "filegroup" | "group" => {
260 if !value.is_empty() {
261 input.group = Some(Id::new(&value).map_err(map_parse_error)?);
262 }
263 }
264 _ => {
265 return Err(ParseError::new(format!("unknown project field `{key}`")));
266 }
267 };
268 }
269
270 Ok(input)
271 }
272
273 pub fn is_all_deps(&self) -> bool {
274 self.project == "^"
275 }
276
277 pub fn get_deps_scope(&self) -> Option<DependencyScope> {
278 match self.project.as_str() {
279 "^dev" | "^development" => Some(DependencyScope::Development),
280 "^prod" | "^production" => Some(DependencyScope::Production),
281 "^peer" => Some(DependencyScope::Peer),
282 "^build" => Some(DependencyScope::Build),
283 _ => None,
284 }
285 }
286}
287
288#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
290#[serde(try_from = "InputShape")]
291pub enum Input {
292 EnvVar(String),
293 EnvVarGlob(String),
294 File(FileInput),
295 FileGroup(FileGroupInput),
296 Glob(GlobInput),
297 Project(ProjectInput),
298 TokenFunc(String),
300 TokenVar(String),
301 }
304
305impl Input {
306 pub fn create_uri(value: &str) -> Result<Uri, ParseError> {
307 let mut value = standardize_separators(value);
309
310 if !value.contains("://") {
312 if is_glob_like(&value) {
313 value = format!("glob://{}", value.replace("?", "__QM__"));
314 } else {
315 value = format!("file://{value}");
316 }
317 }
318
319 Uri::parse(&value)
320 }
321
322 pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> {
323 Self::from_str(value.as_ref())
324 }
325
326 pub fn as_str(&self) -> &str {
327 match self {
328 Self::EnvVar(value)
329 | Self::EnvVarGlob(value)
330 | Self::TokenFunc(value)
331 | Self::TokenVar(value) => value,
332 Self::File(value) => value.file.as_str(),
333 Self::FileGroup(value) => value.group.as_str(),
334 Self::Glob(value) => value.glob.as_str(),
335 Self::Project(value) => value.project.as_str(),
336 }
337 }
338
339 pub fn is_glob(&self) -> bool {
340 matches!(self, Self::EnvVarGlob(_) | Self::Glob(_))
341 }
342}
343
344impl FromStr for Input {
345 type Err = ParseError;
346
347 fn from_str(value: &str) -> Result<Self, Self::Err> {
348 if value.starts_with('@') && patterns::TOKEN_FUNC_DISTINCT.is_match(value) {
350 return Ok(Self::TokenFunc(value.to_owned()));
351 }
352
353 if let Some(var) = value.strip_prefix('$') {
355 if patterns::ENV_VAR_DISTINCT.is_match(value) {
356 return Ok(Self::EnvVar(var.to_owned()));
357 } else if patterns::ENV_VAR_GLOB_DISTINCT.is_match(value) {
358 return Ok(Self::EnvVarGlob(var.to_owned()));
359 } else if patterns::TOKEN_VAR_DISTINCT.is_match(value) {
360 return Ok(Self::TokenVar(value.to_owned()));
361 }
362 }
363
364 let uri = Self::create_uri(value)?;
366
367 match uri.scheme.as_str() {
368 "file" => Ok(Self::File(FileInput::from_uri(uri)?)),
369 "glob" => Ok(Self::Glob(GlobInput::from_uri(uri)?)),
370 "group" | "filegroup" | "fileGroup" => {
371 Ok(Self::FileGroup(FileGroupInput::from_uri(uri)?))
372 }
373 "project" => Ok(Self::Project(ProjectInput::from_uri(uri)?)),
374 other => Err(ParseError::new(format!(
375 "input protocol `{other}://` is not supported"
376 ))),
377 }
378 }
379}
380
381impl Schematic for Input {
382 fn schema_name() -> Option<String> {
383 Some("Input".into())
384 }
385
386 fn build_schema(mut schema: SchemaBuilder) -> Schema {
387 schema.union(UnionType::new_any([
388 schema.infer::<String>(),
389 schema.infer::<FileInput>(),
390 schema.infer::<FileGroupInput>(),
391 schema.infer::<GlobInput>(),
392 schema.infer::<ProjectInput>(),
393 ]))
394 }
395}
396
397impl Serialize for Input {
398 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
399 where
400 S: Serializer,
401 {
402 match self {
403 Input::EnvVar(var) | Input::EnvVarGlob(var) => {
404 serializer.serialize_str(format!("${var}").as_str())
405 }
406 Input::TokenFunc(token) | Input::TokenVar(token) => serializer.serialize_str(token),
407 Input::File(input) => FileInput::serialize(input, serializer),
408 Input::FileGroup(input) => FileGroupInput::serialize(input, serializer),
409 Input::Glob(input) => GlobInput::serialize(input, serializer),
411 Input::Project(input) => ProjectInput::serialize(input, serializer),
412 }
413 }
414}
415
416#[derive(DeserializeUntaggedVerboseError)]
417enum InputShape {
418 String(String),
419 Project(ProjectInput),
421 FileGroup(FileGroupInput),
422 File(FileInput),
423 Glob(GlobInput),
424}
425
426impl TryFrom<InputShape> for Input {
427 type Error = ParseError;
428
429 fn try_from(base: InputShape) -> Result<Self, Self::Error> {
430 match base {
431 InputShape::String(input) => Self::parse(input),
432 InputShape::File(input) => Ok(Self::File(input)),
433 InputShape::FileGroup(input) => Ok(Self::FileGroup(input)),
434 InputShape::Glob(input) => Ok(Self::Glob(input)),
435 InputShape::Project(input) => Ok(Self::Project(input)),
436 }
437 }
438}