moon_config/shapes/
output.rs1use super::portable_path::{FilePath, GlobPath, PortablePath, is_glob_like};
2use super::*;
3use crate::{config_struct, generate_io_file_methods, generate_io_glob_methods, patterns};
4use deserialize_untagged_verbose_error::DeserializeUntaggedVerboseError;
5use moon_common::path::{
6 RelativeFrom, WorkspaceRelativePathBuf, expand_to_workspace_relative, standardize_separators,
7};
8use schematic::{Config, ParseError, Schema, SchemaBuilder, Schematic, schema::UnionType};
9use serde::{Deserialize, Serialize, Serializer};
10use std::str::FromStr;
11
12config_struct!(
13 #[derive(Config)]
15 pub struct FileOutput {
16 pub file: FilePath,
18
19 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub optional: Option<bool>,
23 }
24);
25
26generate_io_file_methods!(FileOutput);
27
28impl FileOutput {
29 pub fn from_uri(uri: Uri) -> Result<Self, ParseError> {
30 let mut output = Self {
31 file: FilePath::parse(&uri.path)?,
32 ..Default::default()
33 };
34
35 for (key, value) in uri.query {
36 match key.as_str() {
37 "optional" => {
38 output.optional = Some(parse_bool_field(&key, &value)?);
39 }
40 _ => {
41 return Err(ParseError::new(format!("unknown file field `{key}`")));
42 }
43 };
44 }
45
46 Ok(output)
47 }
48}
49
50config_struct!(
51 #[derive(Config)]
53 pub struct GlobOutput {
54 pub glob: GlobPath,
56
57 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub optional: Option<bool>,
61 }
62);
63
64generate_io_glob_methods!(GlobOutput);
65
66impl GlobOutput {
67 pub fn from_uri(uri: Uri) -> Result<Self, ParseError> {
68 let mut output = Self {
69 glob: GlobPath::parse(uri.path.replace("__QM__", "?"))?,
70 optional: None,
71 };
72
73 for (key, value) in uri.query {
74 match key.as_str() {
75 "optional" => {
76 output.optional = Some(parse_bool_field(&key, &value)?);
77 }
78 _ => {
79 return Err(ParseError::new(format!("unknown glob field `{key}`")));
80 }
81 };
82 }
83
84 Ok(output)
85 }
86}
87
88#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
90#[serde(try_from = "OutputShape")]
91pub enum Output {
92 File(FileOutput),
93 Glob(GlobOutput),
94 TokenFunc(String),
96 TokenVar(String),
97}
98
99impl Output {
100 pub fn create_uri(value: &str) -> Result<Uri, ParseError> {
101 let mut value = standardize_separators(value);
103
104 if !value.contains("://") {
106 if is_glob_like(&value) {
107 value = format!("glob://{}", value.replace("?", "__QM__"));
108 } else {
109 value = format!("file://{value}");
110 }
111 }
112
113 Uri::parse(&value)
114 }
115
116 pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> {
117 Self::from_str(value.as_ref())
118 }
119
120 pub fn as_str(&self) -> &str {
121 match self {
122 Self::TokenFunc(value) | Self::TokenVar(value) => value,
123 Self::File(value) => value.file.as_str(),
124 Self::Glob(value) => value.glob.as_str(),
125 }
126 }
127
128 pub fn is_glob(&self) -> bool {
129 matches!(self, Self::Glob(_))
130 }
131
132 pub fn is_optional(&self) -> bool {
133 match self {
134 Output::File(value) => value.optional.unwrap_or_default(),
135 Output::Glob(value) => value.optional.unwrap_or_default(),
136 _ => false,
137 }
138 }
139}
140
141impl FromStr for Output {
142 type Err = ParseError;
143
144 fn from_str(value: &str) -> Result<Self, Self::Err> {
145 if value.starts_with('@') && patterns::TOKEN_FUNC_DISTINCT.is_match(value) {
147 return Ok(Self::TokenFunc(value.to_owned()));
148 }
149
150 if value.starts_with('$') {
152 if patterns::ENV_VAR_DISTINCT.is_match(value) {
153 return Err(ParseError::new(
154 "environment variable is not supported by itself",
155 ));
156 } else if patterns::ENV_VAR_GLOB_DISTINCT.is_match(value) {
157 return Err(ParseError::new(
158 "environment variable globs are not supported",
159 ));
160 } else if patterns::TOKEN_VAR_DISTINCT.is_match(value) {
161 return Ok(Self::TokenVar(value.to_owned()));
162 }
163 }
164
165 let uri = Self::create_uri(value)?;
167
168 match uri.scheme.as_str() {
169 "file" => Ok(Self::File(FileOutput::from_uri(uri)?)),
170 "glob" => Ok(Self::Glob(GlobOutput::from_uri(uri)?)),
171 other => Err(ParseError::new(format!(
172 "output protocol `{other}://` is not supported"
173 ))),
174 }
175 }
176}
177
178impl Schematic for Output {
179 fn schema_name() -> Option<String> {
180 Some("Output".into())
181 }
182
183 fn build_schema(mut schema: SchemaBuilder) -> Schema {
184 schema.union(UnionType::new_any([
185 schema.infer::<String>(),
186 schema.infer::<FileOutput>(),
187 schema.infer::<GlobOutput>(),
188 ]))
189 }
190}
191
192impl Serialize for Output {
193 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
194 where
195 S: Serializer,
196 {
197 match self {
198 Output::TokenFunc(token) | Output::TokenVar(token) => serializer.serialize_str(token),
199 Output::File(output) => FileOutput::serialize(output, serializer),
200 Output::Glob(output) => GlobOutput::serialize(output, serializer),
201 }
202 }
203}
204
205#[derive(DeserializeUntaggedVerboseError)]
206enum OutputShape {
207 String(String),
208 File(FileOutput),
210 Glob(GlobOutput),
211}
212
213impl TryFrom<OutputShape> for Output {
214 type Error = ParseError;
215
216 fn try_from(base: OutputShape) -> Result<Self, Self::Error> {
217 match base {
218 OutputShape::String(output) => Self::parse(output),
219 OutputShape::File(output) => Ok(Self::File(output)),
220 OutputShape::Glob(output) => Ok(Self::Glob(output)),
221 }
222 }
223}