1use alloy_primitives::Address;
8use path_slash::PathExt;
9use serde::Deserialize;
10use solar::ast::ContractKind;
11use std::{
12 fs,
13 path::{Component, Path, PathBuf},
14};
15
16pub fn git_source_url(repo: &str, commit: &str, root: &Path, item_path: &Path) -> Option<String> {
27 let repo = repo.trim_end_matches('/');
28 let commit = if commit.is_empty() { "HEAD" } else { commit };
29 let rel = item_path.strip_prefix(root).ok()?;
30 Some(format!("{repo}/blob/{commit}/{}", rel.to_slash_lossy()))
31}
32
33pub fn git_raw_url(repo: &str, commit: &str, root: &Path, item_path: &Path) -> Option<String> {
36 let repo = repo.trim_end_matches('/');
37 let commit = if commit.is_empty() { "HEAD" } else { commit };
38 let rel = item_path.strip_prefix(root).ok()?;
39 Some(format!("{repo}/raw/{commit}/{}", rel.to_slash_lossy()))
40}
41
42#[derive(Clone, Debug, Deserialize)]
46pub struct Deployment {
47 pub address: Address,
49 pub network: Option<String>,
51}
52
53pub fn read_deployments(
62 root: &Path,
63 deployments_dir: Option<&Path>,
64 contract_file: &Path,
65) -> Vec<Deployment> {
66 let dir = root.join(deployments_dir.unwrap_or_else(|| Path::new("deployments")));
67 let Ok(entries) = fs::read_dir(&dir) else {
68 return Vec::new();
69 };
70
71 let mut filename: PathBuf = contract_file.to_path_buf();
73 filename.set_extension("json");
74 let Some(filename) = filename.file_name().map(PathBuf::from) else { return Vec::new() };
75
76 let mut out = Vec::new();
77 for entry in entries.flatten() {
78 let Ok(file_type) = entry.file_type() else { continue };
79 if !file_type.is_dir() {
80 continue;
81 }
82 let Ok(network) = entry.file_name().into_string() else { continue };
83 let path = entry.path().join(&filename);
84 let Ok(content) = fs::read_to_string(&path) else { continue };
85 let Ok(mut deployment) = serde_json::from_str::<Deployment>(&content) else { continue };
86 deployment.network = Some(network);
87 out.push(deployment);
88 }
89 out.sort_by(|a, b| {
91 a.network.as_deref().unwrap_or("").cmp(b.network.as_deref().unwrap_or("")).then_with(|| {
92 let af = format!("{:#x}", a.address);
93 let bf = format!("{:#x}", b.address);
94 af.cmp(&bf)
95 })
96 });
97 out
98}
99
100pub(crate) fn relative_source_path(root: &Path, source: &Path) -> PathBuf {
102 source.strip_prefix(root).map(Path::to_path_buf).unwrap_or_else(|_| {
103 let components = source.components().collect::<Vec<_>>();
104 PathBuf::from("lib")
105 .join(components[components.len().saturating_sub(3)..].iter().collect::<PathBuf>())
106 })
107}
108
109pub(crate) fn is_safe_output_path(path: &Path) -> bool {
111 !path.is_absolute()
112 && !path.components().any(|component| {
113 matches!(component, Component::ParentDir | Component::RootDir | Component::Prefix(_))
114 })
115}
116
117pub(crate) const fn contract_kind_str(kind: ContractKind) -> &'static str {
119 match kind {
120 ContractKind::Contract => "contract",
121 ContractKind::AbstractContract => "abstract",
122 ContractKind::Interface => "interface",
123 ContractKind::Library => "library",
124 }
125}
126
127pub(crate) fn page_path(source: &Path, prefix: &str, name: &str) -> PathBuf {
129 source.parent().unwrap_or(Path::new("")).join(format!("{prefix}.{name}.mdx"))
130}