mod deleted;
mod names;
use std::collections::{BTreeMap, BTreeSet};
use crate::codec::schema::{Supply, field};
use crate::codec::{
Files, MANIFEST, Mode, ReadError, Rec, Role, SpecPath, Tree, Val, canonicalize, check_tree,
parse_files, write_files,
};
use crate::identity::{Key, Label};
use crate::slug::Slug;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Formatted {
pub changed: Files,
pub minted: usize,
pub dropped: Vec<(Label, Key)>,
}
pub fn format(files: &Files) -> Result<Formatted, Vec<ReadError>> {
let mut tree = parse_files(files, Mode::Formatting)?;
let mut supplier = Supplier {
feature_dirs: feature_dirs(&tree),
minted: 0,
};
let mut new_files = Vec::new();
for (role, content) in tree.files.values_mut() {
for record in content.records_mut() {
let unkeyed = record.kind.is_keyed() && record.get(field::KEY).is_none();
supplier.supply(record, role.slug());
if unkeyed && matches!(role, Role::Product { .. } | Role::Feature { .. }) {
new_files.push((role.clone(), key_of(record)));
}
}
}
for (role, key) in new_files {
list_new_file(&mut tree, &role, key.expect("a minted key"));
}
names::resolve(&mut tree)?;
let dropped = deleted::settle(&mut tree);
check_tree(&tree)?;
canonicalize(&mut tree);
let changed = write_files(&tree)
.into_iter()
.filter(|(path, bytes)| files.get(path) != Some(bytes))
.collect();
Ok(Formatted {
changed,
minted: supplier.minted,
dropped,
})
}
fn key_of(record: &Rec) -> Option<Key> {
Key::parse(record.text(field::KEY)?).ok()
}
fn feature_dirs(tree: &Tree) -> BTreeMap<Key, String> {
tree.records(|role| matches!(role, Role::Feature { .. }))
.filter_map(|(_, role, record)| match role {
Role::Feature { group, .. } => Some((key_of(record)?, group.clone())),
_ => None,
})
.collect()
}
struct Supplier {
feature_dirs: BTreeMap<Key, String>,
minted: usize,
}
impl Supplier {
fn supply(&mut self, record: &mut Rec, path_slug: Option<&str>) {
let kind = record.kind;
for spec in kind.fields() {
if record.get(spec.name).is_some() {
continue;
}
match (spec.supply, path_slug) {
(Some(Supply::MintKey), _) => {
record.set_text(spec.name, Key::mint().to_string());
self.minted += 1;
}
(Some(Supply::SlugFromPath), Some(slug)) => record.set_text(spec.name, slug),
_ => {}
}
}
for (_, value) in &mut record.fields {
if let Val::List(items) = value {
self.supply_siblings(items);
}
for child in value.records_mut() {
self.supply(child, None);
}
}
}
fn supply_siblings(&self, items: &mut [Val]) {
let slug_field = |record: &Rec| {
let fields = record.kind.fields().iter();
let mut named = fields.filter(|spec| spec.supply == Some(Supply::SlugFromName));
named.next().map(|spec| spec.name)
};
let mut taken: BTreeSet<Slug> = items
.iter()
.flat_map(Val::records)
.filter_map(|record| Slug::parse(record.text(slug_field(record)?)?).ok())
.collect();
for record in items.iter_mut().flat_map(Val::records_mut) {
if let Some(name) = slug_field(record)
&& record.get(name).is_none()
{
let slug = group_slug(record, &self.feature_dirs, &mut taken);
record.set_text(name, slug);
}
}
}
}
fn group_slug(
group: &Rec,
feature_dirs: &BTreeMap<Key, String>,
taken: &mut BTreeSet<Slug>,
) -> String {
let from_files = group.list(field::FEATURES).iter().find_map(|feature| {
let key = Key::parse(feature.text()?).ok()?;
feature_dirs.get(&key).cloned()
});
from_files.unwrap_or_else(|| {
let name = group.text(field::NAME).unwrap_or_default();
Slug::derive(name, "feature-group")
.unique_in(taken)
.to_string()
})
}
fn list_new_file(tree: &mut Tree, role: &Role, key: Key) {
let (lister, group_slug) = match role {
Role::Product { .. } => (SpecPath::new(MANIFEST), None),
Role::Feature { product, group, .. } => {
let product = product.clone();
(Role::Product { product }.path(), Some(group.as_str()))
}
_ => return,
};
let Some(record) = tree
.files
.get_mut(&lister)
.and_then(|(_, content)| content.records_mut().next())
else {
return;
};
let target = match group_slug {
None => Some((record, field::PRODUCTS)),
Some(slug) => record
.get_mut(field::FEATURE_GROUPS)
.and_then(|groups| {
groups
.records_mut()
.find(|group| group.text(field::SLUG) == Some(slug))
})
.map(|group| (group, field::FEATURES)),
};
if let Some((target, field)) = target {
target.list_mut(field).push(Val::scalar(&key));
}
}