mirror of
https://github.com/Noratrieb/target-tier-docs-experiment.git
synced 2026-01-14 16:35:09 +01:00
214 lines
5.6 KiB
Rust
214 lines
5.6 KiB
Rust
//! Suboptimal half-markdown parser that's just good-enough for this.
|
|
|
|
use eyre::{bail, OptionExt, Result, WrapErr};
|
|
use serde::Deserialize;
|
|
use std::{fs::DirEntry, path::Path};
|
|
|
|
#[derive(Debug, PartialEq, Clone, Deserialize)]
|
|
pub enum Tier {
|
|
#[serde(rename = "1")]
|
|
One,
|
|
#[serde(rename = "2")]
|
|
Two,
|
|
#[serde(rename = "3")]
|
|
Three,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ParsedTargetInfoFile {
|
|
pub pattern: String,
|
|
pub tier: Option<Tier>,
|
|
pub maintainers: Vec<String>,
|
|
pub sections: Vec<(String, String)>,
|
|
pub metadata: Vec<ParsedTargetMetadata>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct Frontmatter {
|
|
tier: Option<Tier>,
|
|
#[serde(default)]
|
|
maintainers: Vec<String>,
|
|
#[serde(default)]
|
|
metadata: Vec<ParsedTargetMetadata>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ParsedTargetMetadata {
|
|
pub pattern: String,
|
|
pub notes: String,
|
|
pub std: TriStateBool,
|
|
pub host: TriStateBool,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TriStateBool {
|
|
True,
|
|
False,
|
|
Unknown,
|
|
}
|
|
|
|
pub fn load_target_infos(directory: &Path) -> Result<Vec<ParsedTargetInfoFile>> {
|
|
let dir = std::fs::read_dir(directory).unwrap();
|
|
let mut infos = Vec::new();
|
|
|
|
for entry in dir {
|
|
let entry = entry?;
|
|
infos.push(
|
|
load_single_target_info(&entry)
|
|
.wrap_err_with(|| format!("loading {}", entry.path().display()))?,
|
|
)
|
|
}
|
|
|
|
Ok(infos)
|
|
}
|
|
|
|
fn load_single_target_info(entry: &DirEntry) -> Result<ParsedTargetInfoFile> {
|
|
let pattern = entry.file_name();
|
|
let name = pattern
|
|
.to_str()
|
|
.ok_or_eyre("file name is invalid utf8")?
|
|
.strip_suffix(".md")
|
|
.ok_or_eyre("target_info files must end with .md")?;
|
|
let content: String = std::fs::read_to_string(entry.path()).wrap_err("reading content")?;
|
|
|
|
parse_file(name, &content)
|
|
}
|
|
|
|
fn parse_file(name: &str, content: &str) -> Result<ParsedTargetInfoFile> {
|
|
let mut frontmatter_splitter = content.split("---\n");
|
|
|
|
let frontmatter = frontmatter_splitter
|
|
.nth(1)
|
|
.ok_or_eyre("missing frontmatter")?;
|
|
|
|
let frontmatter =
|
|
serde_yaml::from_str::<Frontmatter>(frontmatter).wrap_err("invalid frontmatter")?;
|
|
|
|
let body = frontmatter_splitter.next().ok_or_eyre("no body")?;
|
|
|
|
let mut sections = Vec::new();
|
|
|
|
for line in body.lines() {
|
|
if line.starts_with("#") {
|
|
if let Some(header) = line.strip_prefix("## ") {
|
|
if !crate::SECTIONS.contains(&header) {
|
|
bail!(
|
|
"`{header}` is not an allowed section name, must be one of {:?}",
|
|
super::SECTIONS
|
|
);
|
|
}
|
|
sections.push((header.to_owned(), String::new()));
|
|
} else {
|
|
bail!("the only allowed headings are `## `");
|
|
}
|
|
} else {
|
|
match sections.last_mut() {
|
|
Some((_, content)) => {
|
|
content.push_str(line);
|
|
content.push('\n');
|
|
}
|
|
None if line.trim().is_empty() => {}
|
|
None => bail!("line with content not allowed before the first heading"),
|
|
}
|
|
}
|
|
}
|
|
|
|
sections
|
|
.iter_mut()
|
|
.for_each(|section| section.1 = section.1.trim().to_owned());
|
|
|
|
Ok(ParsedTargetInfoFile {
|
|
pattern: name.to_owned(),
|
|
maintainers: frontmatter.maintainers,
|
|
tier: frontmatter.tier,
|
|
sections,
|
|
metadata: frontmatter.metadata,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::parse::Tier;
|
|
|
|
#[test]
|
|
fn no_frontmatter() {
|
|
let name = "archlinux-unknown-linux-gnu.md"; // arch linux is an arch, right?
|
|
let content = "";
|
|
assert!(super::parse_file(name, content).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_section() {
|
|
let name = "6502-nintendo-nes.md";
|
|
let content = "
|
|
---
|
|
---
|
|
|
|
## Not A Real Section
|
|
";
|
|
|
|
assert!(super::parse_file(name, content).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_header() {
|
|
let name = "x86_64-known-linux-gnu.md";
|
|
let content = "
|
|
---
|
|
---
|
|
|
|
# x86_64-known-linux-gnu
|
|
";
|
|
|
|
assert!(super::parse_file(name, content).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn parse_correctly() {
|
|
let name = "cat-unknown-linux-gnu.md";
|
|
let content = r#"
|
|
---
|
|
tier: "1" # first-class cats
|
|
maintainers: ["who maintains the cat?"]
|
|
---
|
|
## Requirements
|
|
|
|
This target mostly just meows and doesn't do much.
|
|
|
|
## Testing
|
|
|
|
You can pet the cat and it might respond positively.
|
|
|
|
## Cross compilation
|
|
|
|
If you're on a dog system, there might be conflicts with the cat, be careful.
|
|
But it should be possible.
|
|
"#;
|
|
|
|
let info = super::parse_file(name, content).unwrap();
|
|
|
|
assert_eq!(info.maintainers, vec!["who maintains the cat?"]);
|
|
assert_eq!(info.pattern, name);
|
|
assert_eq!(info.tier, Some(Tier::One));
|
|
assert_eq!(
|
|
info.sections,
|
|
vec![
|
|
(
|
|
"Requirements".to_owned(),
|
|
"This target mostly just meows and doesn't do much.".to_owned(),
|
|
),
|
|
(
|
|
"Testing".to_owned(),
|
|
"You can pet the cat and it might respond positively.".to_owned(),
|
|
),
|
|
(
|
|
"Cross compilation".to_owned(),
|
|
"If you're on a dog system, there might be conflicts with the cat, be careful.\nBut it should be possible.".to_owned(),
|
|
),
|
|
]
|
|
);
|
|
}
|
|
}
|