1use std::collections::BTreeMap;
16
17use serde::{Deserialize, Serialize};
18
19use crate::edges::{
20 CoAuthorityItem, ConstrainItem, ExtendItem, Origin, ReferenceItem, RefineItem, SupersedeItem,
21};
22use crate::error::{Error, Result};
23use crate::unit::Unit;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum Status {
29 Draft,
30 Approved,
31 Superseded,
32 Retired,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "lowercase")]
38pub enum Risk {
39 Low,
40 Medium,
41 High,
42 Critical,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "kebab-case")]
52pub enum Implementation {
53 Pending,
54 InProgress,
55 Complete,
56 #[serde(rename = "n-a", alias = "n/a")]
57 Na,
58 Deferred,
59}
60
61#[derive(Clone, Debug)]
64pub enum FrontmatterIssue {
65 Malformed(String),
67 UnrepresentableDeclared { key: String, detail: String },
70}
71
72impl From<FrontmatterIssue> for Error {
73 fn from(issue: FrontmatterIssue) -> Self {
74 match issue {
75 FrontmatterIssue::Malformed(m) => Error::Parse(m),
76 FrontmatterIssue::UnrepresentableDeclared { key, detail } => Error::Parse(format!(
77 "declared extra-frontmatter key '{key}' carries an unrepresentable YAML value: {detail}"
78 )),
79 }
80 }
81}
82
83pub const KNOWN_KEYS: &[&str] = &[
86 "id",
88 "title",
89 "status",
90 "created",
91 "summary",
92 "authors",
93 "owner",
94 "kind",
95 "domain",
96 "risk",
97 "implementation",
98 "depends_on",
99 "code_aliases",
100 "feature_branch",
101 "establishes",
103 "extends",
104 "refines",
105 "supersedes",
106 "amends",
107 "co_authority",
108 "constrains",
109 "references",
110 "superseded_by",
112 "retirement_rationale",
113 "amends_sections",
114 "unamendable",
115 "amendment_record",
116 "origin",
118];
119
120#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
126pub struct Frontmatter {
127 pub id: String,
129 pub title: String,
130 pub status: Status,
131 pub created: String,
132 pub summary: String,
133
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub authors: Vec<String>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub owner: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub kind: Option<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub domain: Option<String>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub risk: Option<Risk>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub implementation: Option<Implementation>,
147 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub depends_on: Vec<String>,
149 #[serde(default, skip_serializing_if = "Vec::is_empty")]
150 pub code_aliases: Vec<String>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub feature_branch: Option<String>,
153
154 #[serde(default, skip_serializing_if = "Vec::is_empty")]
156 pub establishes: Vec<Unit>,
157 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 pub extends: Vec<ExtendItem>,
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub refines: Vec<RefineItem>,
161 #[serde(default, skip_serializing_if = "Vec::is_empty")]
162 pub supersedes: Vec<SupersedeItem>,
163 #[serde(default, skip_serializing_if = "Vec::is_empty")]
164 pub amends: Vec<String>,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
166 pub co_authority: Vec<CoAuthorityItem>,
167 #[serde(default, skip_serializing_if = "Vec::is_empty")]
168 pub constrains: Vec<ConstrainItem>,
169 #[serde(default, skip_serializing_if = "Vec::is_empty")]
170 pub references: Vec<ReferenceItem>,
171
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub superseded_by: Option<String>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub retirement_rationale: Option<String>,
177 #[serde(default, skip_serializing_if = "Vec::is_empty")]
178 pub amends_sections: Vec<String>,
179 #[serde(default, skip_serializing_if = "Vec::is_empty")]
180 pub unamendable: Vec<String>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub amendment_record: Option<String>,
183
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub origin: Option<Origin>,
187
188 #[serde(skip)]
190 pub extra_frontmatter: BTreeMap<String, serde_json::Value>,
191}
192
193pub fn split_frontmatter(src: &str) -> Result<(String, String)> {
199 let src = src.strip_prefix('\u{feff}').unwrap_or(src);
200 let mut lines = src.lines();
201
202 match lines.next() {
203 Some(first) if first.trim_end() == "---" => {}
204 _ => {
205 return Err(Error::Parse(
206 "spec.md must begin with a YAML frontmatter block delimited by '---'".into(),
207 ));
208 }
209 }
210
211 let mut frontmatter = String::new();
212 let mut closed = false;
213 for line in lines.by_ref() {
214 if line.trim_end() == "---" {
215 closed = true;
216 break;
217 }
218 frontmatter.push_str(line);
219 frontmatter.push('\n');
220 }
221 if !closed {
222 return Err(Error::Parse(
223 "unterminated frontmatter block (missing closing '---')".into(),
224 ));
225 }
226
227 let mut body = String::new();
228 for line in lines {
229 body.push_str(line);
230 body.push('\n');
231 }
232
233 Ok((frontmatter, body))
234}
235
236pub fn parse_frontmatter(src: &str) -> Result<Frontmatter> {
243 parse_frontmatter_with(src, &[]).map_err(Into::into)
244}
245
246pub fn parse_frontmatter_with(
252 src: &str,
253 declared: &[String],
254) -> std::result::Result<Frontmatter, FrontmatterIssue> {
255 let malformed = |m: String| FrontmatterIssue::Malformed(m);
256 let (yaml, _body) = split_frontmatter(src).map_err(|e| {
257 malformed(match e {
258 Error::Parse(m) => m,
259 other => other.to_string(),
260 })
261 })?;
262
263 let value: serde_yaml::Value = serde_yaml::from_str(&yaml)
264 .map_err(|e| malformed(format!("invalid YAML frontmatter: {e}")))?;
265
266 let mapping = value
267 .as_mapping()
268 .ok_or_else(|| malformed("frontmatter must be a YAML mapping".into()))?;
269
270 let mut frontmatter: Frontmatter = serde_yaml::from_value(value.clone())
272 .map_err(|e| malformed(format!("invalid frontmatter: {e}")))?;
273
274 for (k, v) in mapping {
276 let key = match k.as_str() {
277 Some(s) => s,
278 None => return Err(malformed("frontmatter keys must be strings".into())),
279 };
280 if KNOWN_KEYS.contains(&key) {
281 continue;
282 }
283 let json = if declared.iter().any(|d| d == key) {
284 yaml_to_json(v).map_err(|detail| FrontmatterIssue::UnrepresentableDeclared {
285 key: key.to_string(),
286 detail,
287 })?
288 } else {
289 yaml_to_extra(v).map_err(malformed)?
290 };
291 if json.is_null() {
292 continue;
293 }
294 frontmatter.extra_frontmatter.insert(key.to_string(), json);
295 }
296
297 frontmatter.extends =
301 crate::edges::expand_extend_paths(std::mem::take(&mut frontmatter.extends))
302 .map_err(malformed)?;
303 frontmatter.refines =
304 crate::edges::expand_refine_paths(std::mem::take(&mut frontmatter.refines))
305 .map_err(malformed)?;
306 frontmatter.supersedes =
310 crate::edges::normalize_supersedes(std::mem::take(&mut frontmatter.supersedes));
311
312 Ok(frontmatter)
313}
314
315fn yaml_to_extra(v: &serde_yaml::Value) -> std::result::Result<serde_json::Value, String> {
319 use serde_yaml::Value;
320 match v {
321 Value::Null => Ok(serde_json::Value::Null),
322 Value::Bool(b) => Ok(serde_json::Value::Bool(*b)),
323 Value::Number(n) => {
324 if let Some(i) = n.as_i64() {
325 Ok(serde_json::Value::from(i))
326 } else if let Some(f) = n.as_f64() {
327 serde_json::Number::from_f64(f)
328 .map(serde_json::Value::Number)
329 .ok_or_else(|| "unsupported numeric extra-frontmatter value".to_string())
330 } else {
331 Err("unsupported numeric extra-frontmatter value".to_string())
332 }
333 }
334 Value::String(s) => Ok(serde_json::Value::String(s.clone())),
335 Value::Sequence(seq) => {
336 let mut list = Vec::with_capacity(seq.len());
337 for item in seq {
338 match item.as_str() {
339 Some(s) => list.push(serde_json::Value::String(s.to_string())),
340 None => {
341 return Err("extra-frontmatter lists must contain only strings".to_string());
342 }
343 }
344 }
345 Ok(serde_json::Value::Array(list))
346 }
347 Value::Mapping(_) | Value::Tagged(_) => Err(
348 "extra-frontmatter values must be scalars or string lists, not nested maps".to_string(),
349 ),
350 }
351}
352
353fn yaml_to_json(v: &serde_yaml::Value) -> std::result::Result<serde_json::Value, String> {
359 use serde_yaml::Value;
360 match v {
361 Value::Null => Ok(serde_json::Value::Null),
362 Value::Bool(b) => Ok(serde_json::Value::Bool(*b)),
363 Value::Number(n) => {
364 if let Some(i) = n.as_i64() {
365 Ok(serde_json::Value::from(i))
366 } else if let Some(u) = n.as_u64() {
367 Ok(serde_json::Value::from(u))
368 } else if let Some(f) = n.as_f64() {
369 serde_json::Number::from_f64(f)
370 .map(serde_json::Value::Number)
371 .ok_or_else(|| format!("non-finite number {f} is not JSON-representable"))
372 } else {
373 Err("unsupported YAML number".to_string())
374 }
375 }
376 Value::String(s) => Ok(serde_json::Value::String(s.clone())),
377 Value::Sequence(seq) => seq
378 .iter()
379 .map(yaml_to_json)
380 .collect::<std::result::Result<Vec<_>, _>>()
381 .map(serde_json::Value::Array),
382 Value::Mapping(map) => {
383 let mut out = serde_json::Map::new();
384 for (mk, mv) in map {
385 let Some(key) = mk.as_str() else {
386 return Err("non-string mapping key is not JSON-representable".to_string());
387 };
388 out.insert(key.to_string(), yaml_to_json(mv)?);
389 }
390 Ok(serde_json::Value::Object(out))
391 }
392 Value::Tagged(tagged) => Err(format!(
393 "YAML tag '{}' is not JSON-representable",
394 tagged.tag
395 )),
396 }
397}