use serde_json::Value;
pub(crate) const INPUT_FIELD: &str = "input";
pub(crate) const LOCAL_TARGET: &str = "local";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Mode {
Command,
Agent,
}
impl Mode {
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Command => "command",
Self::Agent => "agent",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Spawn {
mode: Mode,
body: String,
target: Option<String>,
}
impl Spawn {
pub(crate) const fn mode(&self) -> Mode {
self.mode
}
pub(crate) fn body(&self) -> &str {
&self.body
}
pub(crate) fn target(&self) -> Option<&str> {
self.target.as_deref()
}
}
pub(crate) fn parse(input: &Value) -> Result<Spawn, String> {
let raw = input
.get(INPUT_FIELD)
.and_then(Value::as_str)
.ok_or_else(|| {
format!(
"spawn takes one string field, `{INPUT_FIELD}`: a command to run when it starts \
with `!`, otherwise a task to delegate"
)
})?;
read(raw)
}
fn read(raw: &str) -> Result<Spawn, String> {
let trimmed = raw.trim();
let Some(rest) = trimmed.strip_prefix('!') else {
return delegation(trimmed);
};
if rest.starts_with('!') {
return delegation(rest);
}
match rest.strip_prefix('@') {
Some(targeted) => target(targeted),
None => command(rest, None),
}
}
fn target(rest: &str) -> Result<Spawn, String> {
if rest.is_empty() || rest.starts_with(char::is_whitespace) {
return Err(
"`!@` names where a command runs and needs the target right after it: write \
`!@<target> <command>`, as `!@mac xcodebuild -list`"
.to_string(),
);
}
let (name, body) = rest.split_once(char::is_whitespace).unwrap_or((rest, ""));
if !is_target_name(name) {
return Err(format!(
"`{name}` is not a target name: a target is letters, digits, `_` or `-`, as \
`!@mac xcodebuild -list`"
));
}
if name == LOCAL_TARGET {
return Err(format!(
"`{LOCAL_TARGET}` is not a target name: a command with no `@` already runs where \
basis is running, as `!cargo test`"
));
}
if body.trim().is_empty() {
return Err(format!(
"`!@{name}` names a target but no command; write the command after it, as \
`!@{name} cargo test`"
));
}
command(body, Some(name.to_string()))
}
pub(crate) fn is_target_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
fn command(body: &str, target: Option<String>) -> Result<Spawn, String> {
let body = body.trim();
if body.is_empty() {
return Err(
"`!` on its own runs nothing; write the command after it, as `!cargo test`".to_string(),
);
}
Ok(Spawn {
mode: Mode::Command,
body: body.to_string(),
target,
})
}
fn delegation(body: &str) -> Result<Spawn, String> {
let body = body.trim();
if body.is_empty() {
return Err(
"spawn needs something to do: a command after `!`, or a task to delegate".to_string(),
);
}
Ok(Spawn {
mode: Mode::Agent,
body: body.to_string(),
target: None,
})
}