use alloc::collections::BTreeMap;
use core::fmt;
use derive_more::Display;
use schemars::JsonSchema;
use semver::VersionReq;
use serde::de::value::MapAccessDeserializer;
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
pub use crate::format::Format;
pub use crate::part::{Comment, Scope};
use crate::path::RelPath;
pub use crate::settings::{MergeSpec, OnConflict};
use crate::source::{Source, SourceSpec};
pub use crate::vars::{VarName, VarSpec};
pub(crate) const MANIFEST: &str = "profile.toml";
pub(crate) const PAYLOAD: &str = "files";
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
pub profile: Meta,
#[serde(default)]
pub merge: MergeSpec,
#[serde(default)]
pub vars: BTreeMap<VarName, VarSpec>,
#[serde(default)]
pub files: BTreeMap<RelPath, FileSpec>,
}
#[derive(Clone, Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Meta {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
#[schemars(with = "Option<String>")]
pub devset: Option<VersionReq>,
#[serde(default)]
pub requires: Vec<Requirement>,
}
#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)]
#[schemars(untagged)]
pub enum Requirement {
Sibling(String),
Git(#[schemars(with = "SourceSpec")] Source),
}
impl<'de> Deserialize<'de> for Requirement {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visit;
impl<'de> Visitor<'de> for Visit {
type Value = Requirement;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a relative path, or a table naming a git source")
}
fn visit_str<E: de::Error>(self, path: &str) -> Result<Requirement, E> {
Ok(Requirement::Sibling(path.to_owned()))
}
fn visit_map<M: MapAccess<'de>>(self, map: M) -> Result<Requirement, M::Error> {
let spec = SourceSpec::deserialize(MapAccessDeserializer::new(map))?;
if spec.git.is_none() {
return Err(de::Error::custom(
"a requirement table names a git source; a sibling is a relative path",
));
}
Source::try_from(spec).map(Requirement::Git).map_err(de::Error::custom)
}
}
deserializer.deserialize_any(Visit)
}
}
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct FileSpec {
#[serde(default)]
pub policy: Option<Policy>,
#[serde(default)]
pub validate: Option<Format>,
#[serde(default)]
pub template: bool,
#[serde(default)]
pub scope: Scope,
#[serde(default)]
pub comment: Option<Comment>,
#[serde(default)]
pub executable: bool,
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Display,
)]
#[serde(rename_all = "lowercase")]
#[display(rename_all = "lowercase")]
pub enum Policy {
#[default]
Owned,
Merge,
Once,
}
impl Policy {
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde's `skip_serializing_if` passes a reference"
)]
pub(crate) const fn is_owned(&self) -> bool {
matches!(self, Self::Owned)
}
}