devset_core/
collection.rs1use alloc::collections::BTreeMap;
9use core::slice;
10
11use camino::Utf8Path;
12use schemars::JsonSchema;
13use serde::Deserialize;
14
15use crate::errors::{ProfileError, Result};
16use crate::name::{FeatureName, ProfileName, SourceName};
17use crate::path::RelPath;
18use crate::profile::{MANIFEST, Manifest, PAYLOAD};
19use crate::source::{Cache, Oid, Reader, Source};
20use crate::target::from_toml;
21
22pub(crate) const COLLECTION: &str = "collection.toml";
24
25#[derive(Clone, Debug, Deserialize, JsonSchema)]
27#[serde(deny_unknown_fields)]
28pub struct CollectionFile {
29 pub collection: CollectionMeta,
31}
32
33#[derive(Clone, Debug, Deserialize, JsonSchema)]
35#[serde(deny_unknown_fields)]
36pub struct CollectionMeta {
37 pub name: SourceName,
39 #[serde(default)]
41 pub description: Option<String>,
42}
43
44#[derive(Deserialize)]
46struct Head {
47 profile: HeadMeta,
49}
50
51#[derive(Deserialize)]
53struct HeadMeta {
54 name: ProfileName,
56}
57
58#[derive(Debug)]
60pub(crate) struct Index {
61 meta: Option<CollectionMeta>,
63 profiles: BTreeMap<ProfileName, String>,
65}
66
67impl Index {
68 pub(crate) fn read(reader: &Reader, source: &Source) -> Result<Self> {
74 let dirs = profiles(reader.manifests());
75 let paths = dirs.iter().map(|dir| manifest(dir)).collect::<Result<Vec<_>>>()?;
76 let found = reader.read("", &paths)?;
77 let mut profiles = BTreeMap::new();
78 for (dir, path) in dirs.into_iter().zip(&paths) {
79 let Some(raw) = found.get(path) else { continue };
80 let Head { profile } = from_toml(raw, &label(source, path.as_str()))?;
81 if let Some(first) = profiles.insert(profile.name.clone(), dir.clone()) {
82 let (source, name, second) = (source.to_string(), profile.name, dir);
83 return Err(ProfileError::SameNameInSource {
84 location: source,
85 name,
86 first,
87 second,
88 }
89 .into());
90 }
91 }
92 let collection = RelPath::new(COLLECTION)?;
93 let meta = reader.read("", slice::from_ref(&collection))?;
94 let meta = meta
95 .get(&collection)
96 .map(|raw| from_toml::<CollectionFile>(raw, &label(source, COLLECTION)))
97 .transpose()?
98 .map(|file| file.collection);
99 Ok(Self { meta, profiles })
100 }
101
102 pub(crate) fn names(&self) -> impl Iterator<Item = &ProfileName> {
104 self.profiles.keys()
105 }
106
107 pub(crate) fn find(&self, name: &ProfileName, source: &Source) -> Result<&str> {
112 self.profiles.get(name).map(String::as_str).ok_or_else(|| {
113 let profiles = self.profiles.keys().cloned().collect();
114 ProfileError::NotInSource { location: source.to_string(), name: name.clone(), profiles }
115 .into()
116 })
117 }
118}
119
120fn profiles(mut dirs: Vec<String>) -> Vec<String> {
123 dirs.sort();
124 let mut kept: Vec<String> = Vec::with_capacity(dirs.len());
125 for dir in dirs {
126 let payload = kept.iter().any(|profile| {
127 let files = if profile.is_empty() {
128 format!("{PAYLOAD}/")
129 } else {
130 format!("{profile}/{PAYLOAD}/")
131 };
132 dir.starts_with(&files)
133 });
134 if !payload {
135 kept.push(dir);
136 }
137 }
138 kept
139}
140
141fn manifest(dir: &str) -> Result<RelPath> {
143 Ok(if dir.is_empty() {
144 RelPath::new(MANIFEST)?
145 } else {
146 RelPath::new(&format!("{dir}/{MANIFEST}"))?
147 })
148}
149
150pub(crate) fn label(source: &Source, file: &str) -> String {
152 match source {
153 Source::Dir(dir) => dir.join(file).into_string(),
154 Source::Git { url, path, .. } => path
155 .as_ref()
156 .map_or_else(|| format!("{url}/{file}"), |path| format!("{url}/{path}/{file}")),
157 }
158}
159
160#[derive(Debug)]
162pub struct Listing {
163 pub meta: Option<CollectionMeta>,
165 pub profiles: Vec<Summary>,
167}
168
169#[derive(Debug)]
171pub struct Summary {
172 pub manifest: Manifest,
174 pub dir: String,
176}
177
178impl Summary {
179 pub fn features(&self) -> impl Iterator<Item = (&FeatureName, bool)> {
181 let features = &self.manifest.features;
182 features.declared.keys().map(|name| (name, features.default.contains(name)))
183 }
184}
185
186pub fn list(source: &Source, root: &Utf8Path, pin: Option<&Oid>, cache: &Cache) -> Result<Listing> {
193 let reader = source.open(root, pin, cache)?;
194 let Index { meta, profiles } = Index::read(&reader, source)?;
195 let paths = profiles.values().map(|dir| manifest(dir)).collect::<Result<Vec<_>>>()?;
196 let found = reader.read("", &paths)?;
197 let mut listed = Vec::with_capacity(paths.len());
198 for (dir, path) in profiles.into_values().zip(&paths) {
199 let Some(raw) = found.get(path) else { continue };
200 let file = label(source, path.as_str());
201 let manifest: Manifest = from_toml(raw, &file)?;
202 manifest.check(&file)?;
203 listed.push(Summary { manifest, dir });
204 }
205 Ok(Listing { meta, profiles: listed })
206}
207
208#[cfg(test)]
209mod tests {
210 use super::profiles;
211
212 #[test]
213 fn a_manifest_in_a_payload_is_not_a_profile() {
214 let found = profiles(
215 ["b", "a", "a/files/x", "a/extra", "files/y", "c/files", ""]
216 .map(str::to_owned)
217 .to_vec(),
218 );
219 assert_eq!(
220 found,
221 ["", "a", "a/extra", "b", "c/files"],
222 "files/ under a profile is its payload"
223 );
224 }
225}