Skip to main content

forge_doc/
utils.rs

1//! `GitSource` + `Deployments` helpers.
2//!
3//! Pure functions ported from the legacy `forge-doc` preprocessors:
4//! * `git_source_url`: `<repo>/blob/<commit>/<rel>` for a source file.
5//! * `read_deployments`: load `<dir>/<network>/<contract>.json` artifacts.
6
7use alloy_primitives::Address;
8use path_slash::PathExt;
9use serde::Deserialize;
10use std::{
11    fs,
12    path::{Path, PathBuf},
13};
14
15// ── git source ────────────────────────────────────────────────────────────────
16
17/// Build the `{repo}/blob/{commit}/{rel}` URL for a source file.
18///
19/// Returns `None` if `item_path` is not under `root` (i.e. for absolute external paths).
20/// `commit` falls back to `"HEAD"` when empty (GitHub's `blob/HEAD/...` resolves
21/// to the repository's default branch regardless of whether it is `main`,
22/// `master`, or anything else).
23///
24/// Path components are joined with `/` so the URL is well-formed on Windows.
25pub fn git_source_url(repo: &str, commit: &str, root: &Path, item_path: &Path) -> Option<String> {
26    let repo = repo.trim_end_matches('/');
27    let commit = if commit.is_empty() { "HEAD" } else { commit };
28    let rel = item_path.strip_prefix(root).ok()?;
29    Some(format!("{repo}/blob/{commit}/{}", rel.to_slash_lossy()))
30}
31
32/// Return a `{repo}/raw/{commit}/...` URL suitable for embedding binary assets
33/// (images, fonts, etc.) directly rather than the GitHub blob viewer page.
34pub fn git_raw_url(repo: &str, commit: &str, root: &Path, item_path: &Path) -> Option<String> {
35    let repo = repo.trim_end_matches('/');
36    let commit = if commit.is_empty() { "HEAD" } else { commit };
37    let rel = item_path.strip_prefix(root).ok()?;
38    Some(format!("{repo}/raw/{commit}/{}", rel.to_slash_lossy()))
39}
40
41// ── deployments ──────────────────────────────────────────────────────────────
42
43/// A contract deployment entry, deserialised from `<dir>/<network>/<contract>.json`.
44#[derive(Clone, Debug, Deserialize)]
45pub struct Deployment {
46    /// The contract address.
47    pub address: Address,
48    /// The network name (filled in from the parent directory name).
49    pub network: Option<String>,
50}
51
52/// Read all deployments for a Solidity contract file.
53///
54/// Walks `deployments_dir` (defaulting to `<root>/deployments`), looks for a
55/// `<network>/<contract-stem>.json` file in each top-level subdirectory, and
56/// returns the parsed [`Deployment`]s tagged with their network name.
57///
58/// Errors when reading individual entries are silently skipped to mirror the
59/// legacy preprocessor's lenient behaviour.
60pub fn read_deployments(
61    root: &Path,
62    deployments_dir: Option<&Path>,
63    contract_file: &Path,
64) -> Vec<Deployment> {
65    let dir = root.join(deployments_dir.unwrap_or_else(|| Path::new("deployments")));
66    let Ok(entries) = fs::read_dir(&dir) else {
67        return Vec::new();
68    };
69
70    // Switch ".sol" -> ".json" and keep just the file name.
71    let mut filename: PathBuf = contract_file.to_path_buf();
72    filename.set_extension("json");
73    let Some(filename) = filename.file_name().map(PathBuf::from) else { return Vec::new() };
74
75    let mut out = Vec::new();
76    for entry in entries.flatten() {
77        let Ok(file_type) = entry.file_type() else { continue };
78        if !file_type.is_dir() {
79            continue;
80        }
81        let Ok(network) = entry.file_name().into_string() else { continue };
82        let path = entry.path().join(&filename);
83        let Ok(content) = fs::read_to_string(&path) else { continue };
84        let Ok(mut deployment) = serde_json::from_str::<Deployment>(&content) else { continue };
85        deployment.network = Some(network);
86        out.push(deployment);
87    }
88    // Sort for deterministic output across platforms (fs::read_dir order is unspecified).
89    out.sort_by(|a, b| {
90        a.network.as_deref().unwrap_or("").cmp(b.network.as_deref().unwrap_or("")).then_with(|| {
91            let af = format!("{:#x}", a.address);
92            let bf = format!("{:#x}", b.address);
93            af.cmp(&bf)
94        })
95    });
96    out
97}