use alloc::collections::BTreeMap;
use core::slice;
use camino::Utf8Path;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::errors::{ProfileError, Result};
use crate::name::{FeatureName, ProfileName, SourceName};
use crate::path::RelPath;
use crate::profile::{MANIFEST, Manifest, PAYLOAD};
use crate::source::{Cache, Oid, Reader, Source};
use crate::target::from_toml;
pub(crate) const COLLECTION: &str = "collection.toml";
#[derive(Clone, Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CollectionFile {
pub collection: CollectionMeta,
}
#[derive(Clone, Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CollectionMeta {
pub name: SourceName,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Deserialize)]
struct Head {
profile: HeadMeta,
}
#[derive(Deserialize)]
struct HeadMeta {
name: ProfileName,
}
#[derive(Debug)]
pub(crate) struct Index {
meta: Option<CollectionMeta>,
profiles: BTreeMap<ProfileName, String>,
}
impl Index {
pub(crate) fn read(reader: &Reader, source: &Source) -> Result<Self> {
let dirs = profiles(reader.manifests());
let paths = dirs.iter().map(|dir| manifest(dir)).collect::<Result<Vec<_>>>()?;
let found = reader.read("", &paths)?;
let mut profiles = BTreeMap::new();
for (dir, path) in dirs.into_iter().zip(&paths) {
let Some(raw) = found.get(path) else { continue };
let Head { profile } = from_toml(raw, &label(source, path.as_str()))?;
if let Some(first) = profiles.insert(profile.name.clone(), dir.clone()) {
let (source, name, second) = (source.to_string(), profile.name, dir);
return Err(ProfileError::SameNameInSource {
location: source,
name,
first,
second,
}
.into());
}
}
let collection = RelPath::new(COLLECTION)?;
let meta = reader.read("", slice::from_ref(&collection))?;
let meta = meta
.get(&collection)
.map(|raw| from_toml::<CollectionFile>(raw, &label(source, COLLECTION)))
.transpose()?
.map(|file| file.collection);
Ok(Self { meta, profiles })
}
pub(crate) fn names(&self) -> impl Iterator<Item = &ProfileName> {
self.profiles.keys()
}
pub(crate) fn find(&self, name: &ProfileName, source: &Source) -> Result<&str> {
self.profiles.get(name).map(String::as_str).ok_or_else(|| {
let profiles = self.profiles.keys().cloned().collect();
ProfileError::NotInSource { location: source.to_string(), name: name.clone(), profiles }
.into()
})
}
}
fn profiles(mut dirs: Vec<String>) -> Vec<String> {
dirs.sort();
let mut kept: Vec<String> = Vec::with_capacity(dirs.len());
for dir in dirs {
let payload = kept.iter().any(|profile| {
let files = if profile.is_empty() {
format!("{PAYLOAD}/")
} else {
format!("{profile}/{PAYLOAD}/")
};
dir.starts_with(&files)
});
if !payload {
kept.push(dir);
}
}
kept
}
fn manifest(dir: &str) -> Result<RelPath> {
Ok(if dir.is_empty() {
RelPath::new(MANIFEST)?
} else {
RelPath::new(&format!("{dir}/{MANIFEST}"))?
})
}
pub(crate) fn label(source: &Source, file: &str) -> String {
match source {
Source::Dir(dir) => dir.join(file).into_string(),
Source::Git { url, path, .. } => path
.as_ref()
.map_or_else(|| format!("{url}/{file}"), |path| format!("{url}/{path}/{file}")),
}
}
#[derive(Debug)]
pub struct Listing {
pub meta: Option<CollectionMeta>,
pub profiles: Vec<Summary>,
}
#[derive(Debug)]
pub struct Summary {
pub manifest: Manifest,
pub dir: String,
}
impl Summary {
pub fn features(&self) -> impl Iterator<Item = (&FeatureName, bool)> {
let features = &self.manifest.features;
features.declared.keys().map(|name| (name, features.default.contains(name)))
}
}
pub fn list(source: &Source, root: &Utf8Path, pin: Option<&Oid>, cache: &Cache) -> Result<Listing> {
let reader = source.open(root, pin, cache)?;
let Index { meta, profiles } = Index::read(&reader, source)?;
let paths = profiles.values().map(|dir| manifest(dir)).collect::<Result<Vec<_>>>()?;
let found = reader.read("", &paths)?;
let mut listed = Vec::with_capacity(paths.len());
for (dir, path) in profiles.into_values().zip(&paths) {
let Some(raw) = found.get(path) else { continue };
let file = label(source, path.as_str());
let manifest: Manifest = from_toml(raw, &file)?;
manifest.check(&file)?;
listed.push(Summary { manifest, dir });
}
Ok(Listing { meta, profiles: listed })
}
#[cfg(test)]
mod tests {
use super::profiles;
#[test]
fn a_manifest_in_a_payload_is_not_a_profile() {
let found = profiles(
["b", "a", "a/files/x", "a/extra", "files/y", "c/files", ""]
.map(str::to_owned)
.to_vec(),
);
assert_eq!(
found,
["", "a", "a/extra", "b", "c/files"],
"files/ under a profile is its payload"
);
}
}