use std::fmt;
use schemars::JsonSchema;
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer};
use super::{
AbsPath, CommandStep, Copy, FileStep, Firewall, GroupStep, Guard, Invocation, Package, ProjectPath, Repository, Service, Step, Template, UserStep,
};
use crate::syntax::{Code, mistake::Mistake};
const KINDS: &str = "`run`, `copy`, `packages`, `playbook`, `repository`, `file`, `template`, `user`, `group`, `service`, `command` and `firewall`";
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub(super) struct StepInput {
#[serde(default)]
run: Option<ProjectPath>,
#[serde(default)]
creates: Option<AbsPath>,
#[serde(default)]
unless: Option<Invocation>,
#[serde(default)]
copy: Option<Copy>,
#[serde(default)]
packages: Option<Vec<Package>>,
#[serde(default)]
playbook: Option<ProjectPath>,
#[serde(default)]
repository: Option<Repository>,
#[serde(default)]
file: Option<FileStep>,
#[serde(default)]
template: Option<Template>,
#[serde(default)]
user: Option<UserStep>,
#[serde(default)]
group: Option<GroupStep>,
#[serde(default)]
service: Option<Service>,
#[serde(default)]
command: Option<Invocation>,
#[serde(default)]
firewall: Option<Firewall>,
}
#[derive(JsonSchema)]
#[serde(untagged)]
pub(super) enum StepSchema {
Reboot(RebootWord),
Map(Box<StepInput>),
}
#[derive(JsonSchema)]
pub(super) enum RebootWord {
#[serde(rename = "reboot")]
Reboot,
}
impl<'de> Deserialize<'de> for StepSchema {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<StepSchema, D::Error> {
struct V;
impl<'de> Visitor<'de> for V {
type Value = StepSchema;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "`reboot`, or a map with one of {KINDS}")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<StepSchema, E> {
if v == "reboot" {
Ok(StepSchema::Reboot(RebootWord::Reboot))
} else {
Err(E::custom(format!("a step is `reboot` or a map: write `{{ run: {v} }}`")))
}
}
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<StepSchema, A::Error> {
StepInput::deserialize(de::value::MapAccessDeserializer::new(map)).map(|s| StepSchema::Map(Box::new(s)))
}
}
d.deserialize_any(V)
}
}
impl TryFrom<StepSchema> for Step {
type Error = Mistake;
fn try_from(written: StepSchema) -> Result<Step, Mistake> {
match written {
StepSchema::Reboot(RebootWord::Reboot) => Ok(Step::Reboot),
StepSchema::Map(fields) => step(*fields),
}
}
}
fn step(s: StepInput) -> Result<Step, Mistake> {
let invalid = |reason: &str| Mistake::new(Code::InvalidValue, reason);
if s.creates.is_some() && s.run.is_none() && s.command.is_none() {
return Err(invalid("`creates` goes with `run` or `command`"));
}
if s.unless.is_some() && s.command.is_none() {
return Err(invalid("`unless` goes with `command`: a `run` script says `creates`"));
}
let (command, creates) = match s.command {
Some(run) => (
Some(CommandStep {
run,
guard: guard(s.creates, s.unless)?,
}),
None,
),
None => (None, s.creates),
};
if s.packages.as_ref().is_some_and(Vec::is_empty) {
return Err(invalid("name the packages to install"));
}
let mut given: Vec<Step> = [
s.run.map(|script| Step::Run { script, creates }),
s.copy.map(Step::Copy),
s.packages.map(Step::Packages),
s.playbook.map(Step::Playbook),
s.repository.map(Step::Repository),
s.file.map(Step::File),
s.template.map(Step::Template),
s.user.map(Step::User),
s.group.map(Step::Group),
s.service.map(Step::Service),
command.map(Step::Command),
s.firewall.map(Step::Firewall),
]
.into_iter()
.flatten()
.collect();
match given.len() {
1 => Ok(given.remove(0)),
0 => Err(invalid(&format!("a step needs one of {KINDS} (or is `reboot`)"))),
_ => Err(invalid(&format!("a step does one thing: one of {KINDS} per step"))),
}
}
fn guard(creates: Option<AbsPath>, unless: Option<Invocation>) -> Result<Guard, Mistake> {
match (creates, unless) {
(Some(path), None) => Ok(Guard::Creates(path)),
(None, Some(check)) => Ok(Guard::Unless(check)),
(Some(_), Some(_)) => Err(Mistake::new(Code::Conflict, "a command has one guard: `creates` or `unless`")),
(None, None) => Err(Mistake::new(Code::MissingField, "a command runs once; say how to tell it already ran")
.fix("`creates: <path>` or `unless: <check>` beside `command`, or a `run:` script")),
}
}