use std::collections::BTreeMap;
use super::error::{Problem, ReadError, SpecPath};
use super::record::{Mode, Rec, check, to_out};
use super::schema::Kind;
use crate::yaml::{self, Out, Value};
pub type Files = BTreeMap<SpecPath, Vec<u8>>;
pub const MANIFEST: &str = "loftspec.yaml";
pub const LABELS: &str = "labels.yaml";
pub const TAGS: &str = "tags.yaml";
pub const CATEGORIES: &str = "categories.yaml";
pub const LINT: &str = "lint.yaml";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Role {
Manifest,
Labels,
Tags,
Categories,
Lint,
Product {
product: String,
},
Feature {
product: String,
group: String,
feature: String,
},
Component {
slug: String,
},
Decision {
slug: String,
},
}
impl Role {
#[must_use]
pub fn of(path: &SpecPath) -> Option<Self> {
let parts: Vec<&str> = path.parts().collect();
let stem = |name: &str| name.strip_suffix(".yaml").map(str::to_owned);
Some(match parts.as_slice() {
[MANIFEST] => Self::Manifest,
[LABELS] => Self::Labels,
[TAGS] => Self::Tags,
[CATEGORIES] => Self::Categories,
[LINT] => Self::Lint,
["products", product, "product.yaml"] => Self::Product {
product: (*product).to_owned(),
},
["products", product, group, feature] => Self::Feature {
product: (*product).to_owned(),
group: (*group).to_owned(),
feature: stem(feature)?,
},
["components", component] => Self::Component {
slug: stem(component)?,
},
["decisions", decision] => Self::Decision {
slug: stem(decision)?,
},
_ => return None,
})
}
#[must_use]
pub fn path(&self) -> SpecPath {
SpecPath::new(&match self {
Self::Manifest => MANIFEST.to_owned(),
Self::Labels => LABELS.to_owned(),
Self::Tags => TAGS.to_owned(),
Self::Categories => CATEGORIES.to_owned(),
Self::Lint => LINT.to_owned(),
Self::Product { product } => format!("products/{product}/product.yaml"),
Self::Feature {
product,
group,
feature,
} => format!("products/{product}/{group}/{feature}.yaml"),
Self::Component { slug } => format!("components/{slug}.yaml"),
Self::Decision { slug } => format!("decisions/{slug}.yaml"),
})
}
#[must_use]
pub fn slug(&self) -> Option<&str> {
match self {
Self::Product { product } => Some(product),
Self::Feature { feature, .. } => Some(feature),
Self::Component { slug } | Self::Decision { slug } => Some(slug),
_ => None,
}
}
#[must_use]
pub const fn is_written(&self) -> bool {
self.kind().is_some()
}
const fn kind(&self) -> Option<Kind> {
match self {
Self::Manifest => Some(Kind::Manifest),
Self::Labels => Some(Kind::Labels),
Self::Tags => Some(Kind::TagEntry),
Self::Categories => Some(Kind::CategoryEntry),
Self::Lint => None,
Self::Product { .. } => Some(Kind::Product),
Self::Feature { .. } => Some(Kind::Feature),
Self::Component { .. } => Some(Kind::RootComponent),
Self::Decision { .. } => Some(Kind::Decision),
}
}
const fn is_registry(&self) -> bool {
matches!(self, Self::Tags | Self::Categories)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Content {
Record(Rec),
Registry(Vec<Rec>),
Lint(Vec<u8>),
}
impl Content {
pub fn records(&self) -> impl Iterator<Item = &Rec> {
let records: &[Rec] = match self {
Self::Record(record) => std::slice::from_ref(record),
Self::Registry(entries) => entries,
Self::Lint(_) => &[],
};
records.iter()
}
pub fn records_mut(&mut self) -> impl Iterator<Item = &mut Rec> {
let records: &mut [Rec] = match self {
Self::Record(record) => std::slice::from_mut(record),
Self::Registry(entries) => entries,
Self::Lint(_) => &mut [],
};
records.iter_mut()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Tree {
pub files: BTreeMap<SpecPath, (Role, Content)>,
}
impl Tree {
#[must_use]
pub fn record(&self, path: &SpecPath) -> Option<&Rec> {
match self.files.get(path) {
Some((_, Content::Record(record))) => Some(record),
_ => None,
}
}
pub fn records<'a>(
&'a self,
wanted: impl Fn(&Role) -> bool + 'a,
) -> impl Iterator<Item = (&'a SpecPath, &'a Role, &'a Rec)> + 'a {
self.files
.iter()
.filter(move |(_, (role, _))| wanted(role))
.filter_map(|(path, (role, content))| match content {
Content::Record(record) => Some((path, role, record)),
_ => None,
})
}
#[must_use]
pub fn registry(&self, name: &str) -> &[Rec] {
match self.files.get(&SpecPath::new(name)) {
Some((_, Content::Registry(entries))) => entries,
_ => &[],
}
}
}
pub(crate) fn read_file(
path: &SpecPath,
bytes: &[u8],
mode: Mode,
) -> Result<(Role, Content), ReadError> {
let role = Role::of(path).ok_or_else(|| ReadError::at(path, None, Problem::UnknownFile))?;
let node = yaml::read(bytes).map_err(|error| ReadError::yaml(path, error))?;
let Some(kind) = role.kind() else {
return Ok((role, Content::Lint(bytes.to_vec())));
};
let content = match node {
None if role.is_registry() => Content::Registry(Vec::new()),
None => {
return Err(ReadError::at(
path,
None,
Problem::Expected("a record; the file is empty"),
));
}
Some(node) if role.is_registry() => {
let Value::Sequence(items) = &node.value else {
return Err(ReadError::at(
path,
Some(node.position),
Problem::Expected("a list of entries"),
));
};
let entries = items
.iter()
.map(|item| check(item, kind, mode))
.collect::<Result<_, _>>()
.map_err(|located| located.into_error(path))?;
Content::Registry(entries)
}
Some(node) => {
Content::Record(check(&node, kind, mode).map_err(|located| located.into_error(path))?)
}
};
Ok((role, content))
}
#[must_use]
pub fn write_files(tree: &Tree) -> Files {
tree.files
.iter()
.map(|(path, (_, content))| {
let bytes = match content {
Content::Record(record) => yaml::write(&to_out(record)).into_bytes(),
Content::Registry(entries) => {
yaml::write(&Out::Sequence(entries.iter().map(to_out).collect())).into_bytes()
}
Content::Lint(bytes) => bytes.clone(),
};
(path.clone(), bytes)
})
.collect()
}
pub(crate) fn keyed<'a>(record: &'a Rec, found: &mut Vec<&'a Rec>) {
if record.kind.is_keyed() {
found.push(record);
}
for child in record.children() {
keyed(child, found);
}
}