use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::edges::{CoAuthorityItem, ConstrainItem, ExtendItem, Origin, ReferenceItem, RefineItem};
use crate::error::{Error, Result};
use crate::unit::Unit;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Draft,
Approved,
Superseded,
Retired,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Risk {
Low,
Medium,
High,
Critical,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Implementation {
Pending,
InProgress,
Complete,
#[serde(rename = "n-a")]
Na,
Deferred,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ExtraValue {
Bool(bool),
Int(i64),
Float(f64),
Str(String),
List(Vec<String>),
}
pub const KNOWN_KEYS: &[&str] = &[
"id",
"title",
"status",
"created",
"summary",
"authors",
"owner",
"kind",
"domain",
"risk",
"implementation",
"depends_on",
"code_aliases",
"feature_branch",
"establishes",
"extends",
"refines",
"supersedes",
"amends",
"co_authority",
"constrains",
"references",
"superseded_by",
"retirement_rationale",
"amends_sections",
"unamendable",
"amendment_record",
"origin",
];
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Frontmatter {
pub id: String,
pub title: String,
pub status: Status,
pub created: String,
pub summary: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub risk: Option<Risk>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub implementation: Option<Implementation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub depends_on: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub code_aliases: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub feature_branch: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub establishes: Vec<Unit>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub extends: Vec<ExtendItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub refines: Vec<RefineItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supersedes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub amends: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub co_authority: Vec<CoAuthorityItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub constrains: Vec<ConstrainItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub references: Vec<ReferenceItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub superseded_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retirement_rationale: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub amends_sections: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unamendable: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub amendment_record: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub origin: Option<Origin>,
#[serde(skip)]
pub extra_frontmatter: BTreeMap<String, ExtraValue>,
}
pub fn split_frontmatter(src: &str) -> Result<(String, String)> {
let src = src.strip_prefix('\u{feff}').unwrap_or(src);
let mut lines = src.lines();
match lines.next() {
Some(first) if first.trim_end() == "---" => {}
_ => {
return Err(Error::Parse(
"spec.md must begin with a YAML frontmatter block delimited by '---'".into(),
));
}
}
let mut frontmatter = String::new();
let mut closed = false;
for line in lines.by_ref() {
if line.trim_end() == "---" {
closed = true;
break;
}
frontmatter.push_str(line);
frontmatter.push('\n');
}
if !closed {
return Err(Error::Parse(
"unterminated frontmatter block (missing closing '---')".into(),
));
}
let mut body = String::new();
for line in lines {
body.push_str(line);
body.push('\n');
}
Ok((frontmatter, body))
}
pub fn parse_frontmatter(src: &str) -> Result<Frontmatter> {
let (yaml, _body) = split_frontmatter(src)?;
let value: serde_yaml::Value = serde_yaml::from_str(&yaml)
.map_err(|e| Error::Parse(format!("invalid YAML frontmatter: {e}")))?;
let mapping = value
.as_mapping()
.ok_or_else(|| Error::Parse("frontmatter must be a YAML mapping".into()))?;
let mut frontmatter: Frontmatter = serde_yaml::from_value(value.clone())
.map_err(|e| Error::Parse(format!("invalid frontmatter: {e}")))?;
for (k, v) in mapping {
let key = match k.as_str() {
Some(s) => s,
None => return Err(Error::Parse("frontmatter keys must be strings".into())),
};
if KNOWN_KEYS.contains(&key) {
continue;
}
if let Some(extra) = yaml_to_extra(v)? {
frontmatter.extra_frontmatter.insert(key.to_string(), extra);
}
}
Ok(frontmatter)
}
fn yaml_to_extra(v: &serde_yaml::Value) -> Result<Option<ExtraValue>> {
use serde_yaml::Value;
Ok(match v {
Value::Null => None,
Value::Bool(b) => Some(ExtraValue::Bool(*b)),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Some(ExtraValue::Int(i))
} else if let Some(f) = n.as_f64() {
Some(ExtraValue::Float(f))
} else {
return Err(Error::Parse(
"unsupported numeric extra-frontmatter value".into(),
));
}
}
Value::String(s) => Some(ExtraValue::Str(s.clone())),
Value::Sequence(seq) => {
let mut list = Vec::with_capacity(seq.len());
for item in seq {
match item.as_str() {
Some(s) => list.push(s.to_string()),
None => {
return Err(Error::Parse(
"extra-frontmatter lists must contain only strings".into(),
));
}
}
}
Some(ExtraValue::List(list))
}
Value::Mapping(_) | Value::Tagged(_) => {
return Err(Error::Parse(
"extra-frontmatter values must be scalars or string lists, not nested maps".into(),
));
}
})
}