wip: pull request view & md rendering

This commit is contained in:
2026-05-11 00:32:12 +08:00
parent 9f1e051073
commit c29a923e0e
36 changed files with 2716 additions and 99 deletions

166
build.rs
View File

@@ -9,6 +9,12 @@ struct AssetFile {
disk_path: PathBuf,
}
#[derive(Debug)]
struct TimelineFixturePage {
json: String,
end_cursor: Option<String>,
}
fn main() {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
@@ -129,6 +135,9 @@ fn render_github_fixtures(fixture_root: &Path) -> String {
let repo_list = read_json_fixture(&fixture_root.join("repo.list.json"));
let mut issue_fixtures = BTreeMap::<(String, u32), String>::new();
let mut pull_request_fixtures = BTreeMap::<String, String>::new();
let mut pull_request_timeline_fixtures =
BTreeMap::<String, BTreeMap<u32, TimelineFixturePage>>::new();
let mut entries = fs::read_dir(fixture_root)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", fixture_root.display()))
.map(|entry| entry.expect("failed to read github fixture entry"))
@@ -141,11 +150,24 @@ fn render_github_fixtures(fixture_root: &Path) -> String {
.into_string()
.unwrap_or_else(|_| panic!("non-utf8 fixture name in {}", fixture_root.display()));
let Some((filter, page)) = parse_issue_fixture_name(&file_name) else {
if let Some((filter, page)) = parse_issue_fixture_name(&file_name) {
let value = read_fixture_value(&entry.path());
issue_fixtures.insert((filter, page), read_issue_fixture(&value, &entry.path()));
continue;
};
}
issue_fixtures.insert((filter, page), read_issue_fixture(&entry.path()));
if let Some(id) = parse_pull_request_fixture_name(&file_name) {
pull_request_fixtures.insert(id, read_json_fixture(&entry.path()));
continue;
}
if let Some((id, page)) = parse_pull_request_timeline_fixture_name(&file_name) {
let value = read_fixture_value(&entry.path());
pull_request_timeline_fixtures
.entry(id)
.or_default()
.insert(page, read_timeline_fixture(&value, &entry.path()));
}
}
let mut output = String::new();
@@ -177,23 +199,68 @@ fn render_github_fixtures(fixture_root: &Path) -> String {
output.push_str(" }\n");
output.push_str("}\n");
output.push_str("\n");
output.push_str("pub fn issues_pull_request(id: &str) -> Option<&'static str> {\n");
output.push_str(" match id {\n");
for (id, json) in pull_request_fixtures {
output.push_str(" ");
output.push_str(&string_literal(&id));
output.push_str(" => Some(");
output.push_str(&string_literal(&json));
output.push_str("),\n");
}
output.push_str(" _ => None,\n");
output.push_str(" }\n");
output.push_str("}\n");
output.push_str("\n");
output.push_str("pub fn issues_pull_request_timeline(id: &str, after: Option<&str>) -> Option<&'static str> {\n");
output.push_str(" match (id, after) {\n");
for (id, pages) in pull_request_timeline_fixtures {
let mut previous_page = 0;
let mut previous_end_cursor = None;
for (page, fixture) in pages {
if page != previous_page + 1 {
panic!("missing pull request timeline fixture page {page} for {id}");
}
output.push_str(" (");
output.push_str(&string_literal(&id));
output.push_str(", ");
match previous_end_cursor.as_deref() {
Some(after) => output.push_str(&format!("Some({})", string_literal(after))),
None => output.push_str("None"),
}
output.push_str(") => Some(");
output.push_str(&string_literal(&fixture.json));
output.push_str("),\n");
previous_page = page;
previous_end_cursor = fixture.end_cursor.clone();
}
}
output.push_str(" _ => None,\n");
output.push_str(" }\n");
output.push_str("}\n");
output
}
fn read_json_fixture(path: &Path) -> String {
fn read_fixture_value(path: &Path) -> serde_json::Value {
let raw = fs::read_to_string(path)
.unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", path.display()));
let value: serde_json::Value = serde_json::from_str(&raw)
.unwrap_or_else(|err| panic!("invalid json fixture {}: {err}", path.display()));
serde_json::from_str(&raw)
.unwrap_or_else(|err| panic!("invalid json fixture {}: {err}", path.display()))
}
fn read_json_fixture(path: &Path) -> String {
let value = read_fixture_value(path);
serde_json::to_string(&value)
.unwrap_or_else(|err| panic!("failed to serialize fixture {}: {err}", path.display()))
}
fn read_issue_fixture(path: &Path) -> String {
let raw = fs::read_to_string(path)
.unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", path.display()));
let value: serde_json::Value = serde_json::from_str(&raw)
.unwrap_or_else(|err| panic!("invalid json fixture {}: {err}", path.display()));
fn read_issue_fixture(value: &serde_json::Value, path: &Path) -> String {
let issues = value
.as_array()
.unwrap_or_else(|| panic!("issue fixture {} must be a json array", path.display()));
@@ -215,8 +282,49 @@ fn read_issue_fixture(path: &Path) -> String {
})
}
fn read_timeline_fixture(value: &serde_json::Value, path: &Path) -> TimelineFixturePage {
let page_info = value
.get("node")
.and_then(|node| node.get("timelineItems"))
.and_then(|timeline| timeline.get("pageInfo"))
.unwrap_or_else(|| {
panic!(
"timeline fixture {} must include node.timelineItems.pageInfo",
path.display()
)
});
if !matches!(
value
.get("node")
.and_then(|node| node.get("timelineItems"))
.and_then(|timeline| timeline.get("nodes")),
Some(serde_json::Value::Array(_))
) {
panic!(
"timeline fixture {} must include node.timelineItems.nodes",
path.display()
);
}
let end_cursor = page_info
.get("endCursor")
.and_then(serde_json::Value::as_str)
.map(str::to_owned);
let json = serde_json::to_string(value).unwrap_or_else(|err| {
panic!(
"failed to serialize mapped timeline fixture {}: {err}",
path.display()
)
});
TimelineFixturePage { json, end_cursor }
}
fn map_issue_fixture(issue: &serde_json::Value) -> serde_json::Value {
serde_json::json!({
"id": issue_fixture_graphql_id(issue),
"title": required_string(issue, &["title"]),
"state": issue_fixture_state(issue),
"is_draft": issue.get("draft").and_then(serde_json::Value::as_bool).unwrap_or(false),
@@ -231,27 +339,22 @@ fn issue_fixture_state(issue: &serde_json::Value) -> &'static str {
.and_then(serde_json::Value::as_str)
.is_some()
{
return "Merged";
return "MERGED";
}
match required_string(issue, &["state"]) {
"open" => "Open",
"closed" => "Closed",
_ => "Unknown",
"open" => "OPEN",
"closed" => "CLOSED",
state => panic!("unsupported pull request state in fixture: {state}"),
}
}
fn issue_fixture_graphql_id<'a>(issue: &'a serde_json::Value) -> &'a str {
required_string(issue, &["node_id"])
}
fn issue_fixture_cursor(issue: &serde_json::Value) -> Option<String> {
issue
.get("node_id")
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
.or_else(|| {
issue
.get("id")
.and_then(serde_json::Value::as_u64)
.map(|id| id.to_string())
})
Some(issue_fixture_graphql_id(issue).to_owned())
}
fn required_string<'a>(value: &'a serde_json::Value, path: &[&str]) -> &'a str {
@@ -276,6 +379,19 @@ fn parse_issue_fixture_name(file_name: &str) -> Option<(String, u32)> {
Some((filter.to_owned(), page))
}
fn parse_pull_request_fixture_name(file_name: &str) -> Option<String> {
let name = file_name.strip_suffix(".json")?;
name.strip_prefix("issues.pull_request.").map(str::to_owned)
}
fn parse_pull_request_timeline_fixture_name(file_name: &str) -> Option<(String, u32)> {
let name = file_name.strip_suffix(".json")?;
let rest = name.strip_prefix("issues.pull_request_timeline.")?;
let (id, page) = rest.rsplit_once(".page")?;
let page = page.parse::<u32>().ok()?;
Some((id.to_owned(), page))
}
fn string_literal(value: &str) -> String {
format!("{value:?}")
}