use std::collections::{BTreeMap, BTreeSet}; use std::env; use std::fs; use std::path::{Path, PathBuf}; #[derive(Debug)] struct AssetFile { virtual_path: String, disk_path: PathBuf, } fn main() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR")); let asset_root = manifest_dir.join("src/asset"); let fixture_root = manifest_dir.join("fixtures/github"); let out_dir = PathBuf::from(env::var("OUT_DIR").expect("missing OUT_DIR")); let asset_out_file = out_dir.join("asset.rs"); let fixture_out_file = out_dir.join("github_fixtures.rs"); println!("cargo::rerun-if-changed={}", asset_root.display()); println!("cargo::rerun-if-changed={}", fixture_root.display()); let mut directory_entries = BTreeMap::>::new(); directory_entries .entry(String::new()) .or_default() .insert(String::from("asset")); let mut asset_files = Vec::new(); collect_assets( &asset_root, "asset", &mut directory_entries, &mut asset_files, ); let generated = render_assets(&asset_files, &directory_entries); let fixture_module = render_github_fixtures(&fixture_root); fs::write(asset_out_file, generated).expect("failed to write generated assets module"); fs::write(fixture_out_file, fixture_module) .expect("failed to write generated github fixtures module"); } fn collect_assets( disk_dir: &Path, virtual_dir: &str, directory_entries: &mut BTreeMap>, asset_files: &mut Vec, ) { let mut entries = fs::read_dir(disk_dir) .unwrap_or_else(|err| panic!("failed to read {}: {err}", disk_dir.display())) .map(|entry| entry.expect("failed to read directory entry")) .collect::>(); entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let file_name = entry .file_name() .into_string() .unwrap_or_else(|_| panic!("non-utf8 asset name in {}", disk_dir.display())); let child_virtual_path = format!("{virtual_dir}/{file_name}"); let path = entry.path(); directory_entries .entry(String::from(virtual_dir)) .or_default() .insert(file_name); if path.is_dir() { collect_assets(&path, &child_virtual_path, directory_entries, asset_files); } else if path.is_file() { asset_files.push(AssetFile { virtual_path: child_virtual_path, disk_path: path, }); } } } fn render_assets( asset_files: &[AssetFile], directory_entries: &BTreeMap>, ) -> String { let mut output = String::new(); output.push_str( "pub fn load_asset(path: &str) -> gpui::Result>> {\n", ); output.push_str(" match path {\n"); for file in asset_files { output.push_str(" "); output.push_str(&string_literal(&file.virtual_path)); output.push_str(" => Ok(Some(std::borrow::Cow::Borrowed(include_bytes!("); output.push_str(&string_literal(&file.disk_path.to_string_lossy())); output.push_str(")))),\n"); } output.push_str(" _ => Err(anyhow::anyhow!(\"asset not found: {path}\")),\n"); output.push_str(" }\n"); output.push_str("}\n\n"); output.push_str("pub fn list_assets(path: &str) -> gpui::Result> {\n"); output.push_str(" let normalized = path.trim_end_matches('/');\n"); output.push_str(" let normalized = if normalized == \".\" { \"\" } else { normalized };\n"); output.push_str(" let entries: &[&str] = match normalized {\n"); for (directory, entries) in directory_entries { output.push_str(" "); output.push_str(&string_literal(directory)); output.push_str(" => &[\n"); for entry in entries { output.push_str(" "); output.push_str(&string_literal(entry)); output.push_str(",\n"); } output.push_str(" ],\n"); } output.push_str(" _ => &[],\n"); output.push_str(" };\n"); output.push_str(" Ok(entries.iter().copied().map(gpui::SharedString::from).collect())\n"); output.push_str("}\n"); output } fn render_github_fixtures(fixture_root: &Path) -> String { let user_fetch = read_json_fixture(&fixture_root.join("user.fetch.json")); let repo_list = read_json_fixture(&fixture_root.join("repo.list.json")); let mut issue_fixtures = BTreeMap::<(String, u32), String>::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")) .collect::>(); entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let file_name = entry .file_name() .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 { continue; }; issue_fixtures.insert((filter, page), read_issue_fixture(&entry.path())); } let mut output = String::new(); output.push_str("pub fn user_fetch() -> &'static str {\n"); output.push_str(" "); output.push_str(&string_literal(&user_fetch)); output.push_str("\n}\n\n"); output.push_str("pub fn repo_list() -> &'static str {\n"); output.push_str(" "); output.push_str(&string_literal(&repo_list)); output.push_str("\n}\n\n"); output.push_str( "pub fn issues_pull_requests(filter: &str, page: u32) -> Option<&'static str> {\n", ); output.push_str(" match (filter, page) {\n"); for ((filter, page), json) in issue_fixtures { output.push_str(" ("); output.push_str(&string_literal(&filter)); output.push_str(", "); output.push_str(&page.to_string()); 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 } fn read_json_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())); 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())); let issues = value .as_array() .unwrap_or_else(|| panic!("issue fixture {} must be a json array", path.display())); let items = issues.iter().map(map_issue_fixture).collect::>(); let start_cursor = issues.first().and_then(issue_fixture_cursor); let end_cursor = issues.last().and_then(issue_fixture_cursor); serde_json::to_string(&serde_json::json!({ "items": items, "start_cursor": start_cursor, "end_cursor": end_cursor, })) .unwrap_or_else(|err| { panic!( "failed to serialize mapped issue fixture {}: {err}", path.display() ) }) } fn map_issue_fixture(issue: &serde_json::Value) -> serde_json::Value { serde_json::json!({ "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), "repo_slug": required_string(issue, &["repository", "full_name"]), }) } fn issue_fixture_state(issue: &serde_json::Value) -> &'static str { if issue .get("pull_request") .and_then(|pull_request| pull_request.get("merged_at")) .and_then(serde_json::Value::as_str) .is_some() { return "Merged"; } match required_string(issue, &["state"]) { "open" => "Open", "closed" => "Closed", _ => "Unknown", } } fn issue_fixture_cursor(issue: &serde_json::Value) -> Option { 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()) }) } fn required_string<'a>(value: &'a serde_json::Value, path: &[&str]) -> &'a str { let mut current = value; for key in path { current = current .get(*key) .unwrap_or_else(|| panic!("missing key {} in fixture", path.join("."))); } current .as_str() .unwrap_or_else(|| panic!("expected string at {} in fixture", path.join("."))) } fn parse_issue_fixture_name(file_name: &str) -> Option<(String, u32)> { let name = file_name.strip_suffix(".json")?; let rest = name.strip_prefix("issues.pull_requests.")?; let (filter, page) = rest.rsplit_once(".page")?; let page = page.parse::().ok()?; Some((filter.to_owned(), page)) } fn string_literal(value: &str) -> String { format!("{value:?}") }