1use alloc::collections::BTreeMap;
17use core::fmt;
18
19use derive_more::Display;
20use schemars::JsonSchema;
21use semver::VersionReq;
22use serde::de::value::MapAccessDeserializer;
23use serde::de::{self, MapAccess, Visitor};
24use serde::{Deserialize, Deserializer, Serialize};
25
26pub use crate::format::Format;
27pub use crate::part::{Comment, Scope};
28use crate::path::RelPath;
29pub use crate::settings::{MergeSpec, OnConflict};
30use crate::source::{Source, SourceSpec};
31pub use crate::vars::{VarName, VarSpec};
32
33pub(crate) const MANIFEST: &str = "profile.toml";
35
36pub(crate) const PAYLOAD: &str = "files";
38
39#[derive(Debug, Deserialize, JsonSchema)]
41#[serde(deny_unknown_fields)]
42pub struct Manifest {
43 pub profile: Meta,
45 #[serde(default)]
47 pub merge: MergeSpec,
48 #[serde(default)]
50 pub vars: BTreeMap<VarName, VarSpec>,
51 #[serde(default)]
53 pub files: BTreeMap<RelPath, FileSpec>,
54}
55
56#[derive(Clone, Debug, Deserialize, JsonSchema)]
58#[serde(deny_unknown_fields)]
59pub struct Meta {
60 pub name: String,
62 #[serde(default)]
64 pub version: Option<String>,
65 #[serde(default)]
67 pub description: Option<String>,
68 #[serde(default)]
70 #[schemars(with = "Option<String>")]
71 pub devset: Option<VersionReq>,
72 #[serde(default)]
74 pub requires: Vec<Requirement>,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)]
82#[schemars(untagged)]
83pub enum Requirement {
84 Sibling(String),
86 Git(#[schemars(with = "SourceSpec")] Source),
88}
89
90impl<'de> Deserialize<'de> for Requirement {
91 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
92 struct Visit;
94
95 impl<'de> Visitor<'de> for Visit {
96 type Value = Requirement;
97
98 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.write_str("a relative path, or a table naming a git source")
100 }
101
102 fn visit_str<E: de::Error>(self, path: &str) -> Result<Requirement, E> {
103 Ok(Requirement::Sibling(path.to_owned()))
104 }
105
106 fn visit_map<M: MapAccess<'de>>(self, map: M) -> Result<Requirement, M::Error> {
107 let spec = SourceSpec::deserialize(MapAccessDeserializer::new(map))?;
108 if spec.git.is_none() {
109 return Err(de::Error::custom(
110 "a requirement table names a git source; a sibling is a relative path",
111 ));
112 }
113 Source::try_from(spec).map(Requirement::Git).map_err(de::Error::custom)
114 }
115 }
116
117 deserializer.deserialize_any(Visit)
118 }
119}
120
121#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
123#[serde(deny_unknown_fields)]
124pub struct FileSpec {
125 #[serde(default)]
127 pub policy: Option<Policy>,
128 #[serde(default)]
130 pub validate: Option<Format>,
131 #[serde(default)]
133 pub template: bool,
134 #[serde(default)]
136 pub scope: Scope,
137 #[serde(default)]
139 pub comment: Option<Comment>,
140 #[serde(default)]
142 pub executable: bool,
143}
144
145#[derive(
147 Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Display,
148)]
149#[serde(rename_all = "lowercase")]
150#[display(rename_all = "lowercase")]
151pub enum Policy {
152 #[default]
154 Owned,
155 Merge,
157 Once,
159}
160
161impl Policy {
162 #[expect(
164 clippy::trivially_copy_pass_by_ref,
165 reason = "serde's `skip_serializing_if` passes a reference"
166 )]
167 pub(crate) const fn is_owned(&self) -> bool {
168 matches!(self, Self::Owned)
169 }
170}