1#[cfg(not(target_arch = "wasm32"))]
7pub mod git;
8#[cfg(not(target_arch = "wasm32"))]
9pub mod http;
10#[cfg(not(target_arch = "wasm32"))]
11pub mod onchain;
12pub mod path;
13
14use crate::file::ImportSource;
15use std::path::PathBuf;
16
17#[derive(Debug, Clone)]
23pub struct FetcherConfig {
24 pub allow_path: bool,
26 pub allow_git: bool,
28 pub allow_http: bool,
30 pub allow_onchain: bool,
32
33 pub git_config: GitFetcherConfig,
35
36 pub onchain_config: OnchainFetcherConfig,
38
39 pub cache_config: CacheConfig,
41}
42
43impl Default for FetcherConfig {
44 fn default() -> Self {
45 Self::cli_default()
46 }
47}
48
49impl FetcherConfig {
50 pub fn cli_default() -> Self {
52 Self {
53 allow_path: true,
54 allow_git: true,
55 allow_http: true,
56 allow_onchain: true,
57 git_config: GitFetcherConfig::default(),
58 onchain_config: OnchainFetcherConfig::default(),
59 cache_config: CacheConfig::default(),
60 }
61 }
62
63 pub fn wasm_default() -> Self {
65 Self {
66 allow_path: false,
67 allow_git: false,
68 allow_http: false,
69 allow_onchain: false,
70 git_config: GitFetcherConfig::default(),
71 onchain_config: OnchainFetcherConfig::default(),
72 cache_config: CacheConfig::disabled(),
73 }
74 }
75
76 pub fn production_build() -> Self {
78 Self {
79 allow_path: false,
80 allow_git: false,
81 allow_http: false,
82 allow_onchain: true,
83 git_config: GitFetcherConfig::default(),
84 onchain_config: OnchainFetcherConfig::default(),
85 cache_config: CacheConfig::default(),
86 }
87 }
88
89 pub fn local_only() -> Self {
91 Self {
92 allow_path: true,
93 allow_git: false,
94 allow_http: false,
95 allow_onchain: false,
96 git_config: GitFetcherConfig::default(),
97 onchain_config: OnchainFetcherConfig::default(),
98 cache_config: CacheConfig::disabled(),
99 }
100 }
101
102 pub fn is_allowed(&self, source: &ImportSource) -> bool {
104 match source {
105 ImportSource::Path { .. } => self.allow_path,
106 ImportSource::Git { .. } => self.allow_git,
107 ImportSource::Http { .. } => self.allow_http,
108 ImportSource::Onchain { .. } => self.allow_onchain,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Default)]
115pub struct GitFetcherConfig {
116 pub ssh_key_path: Option<PathBuf>,
118 pub use_credential_helper: bool,
120 pub proxy: Option<String>,
122 pub timeout_seconds: u64,
124}
125
126impl GitFetcherConfig {
127 pub fn new() -> Self {
129 Self {
130 ssh_key_path: None,
131 use_credential_helper: true,
132 proxy: None,
133 timeout_seconds: 60,
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
140pub struct OnchainFetcherConfig {
141 pub rpc_endpoints: std::collections::HashMap<String, String>,
143 pub default_network: String,
145 pub timeout_seconds: u64,
147 pub abi_manager_program_id: String,
149 pub abi_manager_is_ephemeral: bool,
151}
152
153impl Default for OnchainFetcherConfig {
154 fn default() -> Self {
155 let mut rpc_endpoints = std::collections::HashMap::new();
156 rpc_endpoints.insert(
157 "mainnet".to_string(),
158 "https://rpc.thru.network".to_string(),
159 );
160 rpc_endpoints.insert(
161 "testnet".to_string(),
162 "https://rpc-testnet.thru.network".to_string(),
163 );
164
165 Self {
166 rpc_endpoints,
167 default_network: "mainnet".to_string(),
168 timeout_seconds: 30,
169 abi_manager_program_id: "taAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACrG7".to_string(),
170 abi_manager_is_ephemeral: false,
171 }
172 }
173}
174
175impl OnchainFetcherConfig {
176 pub fn get_endpoint(&self, network: &str) -> Option<&str> {
178 self.rpc_endpoints.get(network).map(|s| s.as_str())
179 }
180
181 pub fn set_endpoint(&mut self, network: impl Into<String>, endpoint: impl Into<String>) {
183 self.rpc_endpoints.insert(network.into(), endpoint.into());
184 }
185}
186
187#[derive(Debug, Clone)]
189pub struct CacheConfig {
190 pub enabled: bool,
192 pub cache_dir: PathBuf,
194 pub max_age_seconds: u64,
196}
197
198impl Default for CacheConfig {
199 fn default() -> Self {
200 Self {
201 enabled: true,
202 cache_dir: default_cache_dir(),
203 max_age_seconds: 3600, }
205 }
206}
207
208#[cfg(not(target_arch = "wasm32"))]
209fn default_cache_dir() -> PathBuf {
210 dirs::home_dir()
211 .unwrap_or_else(|| PathBuf::from("."))
212 .join(".thru")
213 .join("abi-cache")
214}
215
216#[cfg(target_arch = "wasm32")]
217fn default_cache_dir() -> PathBuf {
218 PathBuf::new()
219}
220
221impl CacheConfig {
222 pub fn disabled() -> Self {
224 Self {
225 enabled: false,
226 cache_dir: PathBuf::new(),
227 max_age_seconds: 0,
228 }
229 }
230
231 pub fn with_dir(cache_dir: PathBuf) -> Self {
233 Self {
234 enabled: true,
235 cache_dir,
236 max_age_seconds: 3600,
237 }
238 }
239}
240
241#[derive(Debug, Clone)]
247pub struct FetchContext {
248 pub base_path: Option<PathBuf>,
250 pub parent_is_remote: bool,
252 pub include_dirs: Vec<PathBuf>,
254}
255
256impl FetchContext {
257 pub fn for_root(file_path: Option<PathBuf>, include_dirs: Vec<PathBuf>) -> Self {
259 Self {
260 base_path: file_path,
261 parent_is_remote: false,
262 include_dirs,
263 }
264 }
265
266 pub fn child_context(&self, source: &ImportSource, resolved_path: Option<PathBuf>) -> Self {
268 Self {
269 base_path: resolved_path,
270 parent_is_remote: source.is_remote(),
271 include_dirs: self.include_dirs.clone(),
272 }
273 }
274}
275
276#[derive(Debug, Clone)]
282pub struct FetchResult {
283 pub content: String,
285 pub canonical_location: String,
287 pub is_remote: bool,
289 pub resolved_path: Option<PathBuf>,
291}
292
293#[derive(Debug)]
299pub enum FetchError {
300 UnsupportedSource(String),
302 NotAllowed(ImportSource),
304 LocalFromRemote(String),
306 NotFound(String),
308 Io(std::io::Error),
310 Git(String),
312 Http { status: u16, message: String },
314 Onchain(String),
316 Parse(String),
318 UnknownNetwork(String),
320 RevisionMismatch { required: String, actual: u64 },
322}
323
324impl std::fmt::Display for FetchError {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 match self {
327 FetchError::UnsupportedSource(s) => write!(f, "Unsupported import source: {}", s),
328 FetchError::NotAllowed(s) => write!(f, "Import type not allowed: {:?}", s),
329 FetchError::LocalFromRemote(s) => {
330 write!(f, "Local import '{}' not allowed from remote source", s)
331 }
332 FetchError::NotFound(s) => write!(f, "Import not found: {}", s),
333 FetchError::Io(e) => write!(f, "IO error: {}", e),
334 FetchError::Git(s) => write!(f, "Git error: {}", s),
335 FetchError::Http { status, message } => {
336 write!(f, "HTTP error {}: {}", status, message)
337 }
338 FetchError::Onchain(s) => write!(f, "On-chain fetch error: {}", s),
339 FetchError::Parse(s) => write!(f, "Parse error: {}", s),
340 FetchError::UnknownNetwork(s) => write!(f, "Unknown network: {}", s),
341 FetchError::RevisionMismatch { required, actual } => {
342 write!(
343 f,
344 "Revision mismatch: required {}, got {}",
345 required, actual
346 )
347 }
348 }
349 }
350}
351
352impl std::error::Error for FetchError {
353 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
354 match self {
355 FetchError::Io(e) => Some(e),
356 _ => None,
357 }
358 }
359}
360
361impl From<std::io::Error> for FetchError {
362 fn from(e: std::io::Error) -> Self {
363 FetchError::Io(e)
364 }
365}
366
367pub trait ImportFetcher: Send + Sync {
373 fn handles(&self, source: &ImportSource) -> bool;
375
376 fn fetch(&self, source: &ImportSource, ctx: &FetchContext) -> Result<FetchResult, FetchError>;
378}
379
380pub struct CompositeFetcher {
386 fetchers: Vec<Box<dyn ImportFetcher>>,
387 config: FetcherConfig,
388}
389
390impl CompositeFetcher {
391 pub fn new(config: FetcherConfig) -> Result<Self, FetchError> {
393 let mut fetchers: Vec<Box<dyn ImportFetcher>> = Vec::new();
394
395 if config.allow_path {
396 fetchers.push(Box::new(path::PathFetcher::new()));
397 }
398 #[cfg(not(target_arch = "wasm32"))]
399 if config.allow_git {
400 fetchers.push(Box::new(git::GitFetcher::new(&config.git_config)));
401 }
402 #[cfg(not(target_arch = "wasm32"))]
403 if config.allow_http {
404 fetchers.push(Box::new(http::HttpFetcher::new()?));
405 }
406 #[cfg(not(target_arch = "wasm32"))]
407 if config.allow_onchain {
408 fetchers.push(Box::new(onchain::OnchainFetcher::new(
409 &config.onchain_config,
410 )));
411 }
412
413 Ok(Self { fetchers, config })
414 }
415
416 pub fn fetch(
418 &self,
419 source: &ImportSource,
420 ctx: &FetchContext,
421 ) -> Result<FetchResult, FetchError> {
422 if !self.config.is_allowed(source) {
424 return Err(FetchError::NotAllowed(source.clone()));
425 }
426
427 for fetcher in &self.fetchers {
429 if fetcher.handles(source) {
430 return fetcher.fetch(source, ctx);
431 }
432 }
433
434 Err(FetchError::UnsupportedSource(format!("{:?}", source)))
435 }
436
437 pub fn config(&self) -> &FetcherConfig {
439 &self.config
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn test_fetcher_config_is_allowed() {
449 let config = FetcherConfig::local_only();
450
451 let path_import = ImportSource::Path {
452 path: "test.abi.yaml".to_string(),
453 };
454 let git_import = ImportSource::Git {
455 url: "https://github.com/test/repo".to_string(),
456 git_ref: "main".to_string(),
457 path: "abi.yaml".to_string(),
458 };
459
460 assert!(config.is_allowed(&path_import));
461 assert!(!config.is_allowed(&git_import));
462 }
463
464 #[test]
465 fn test_cache_config_default() {
466 let config = CacheConfig::default();
467 assert!(config.enabled);
468 assert!(config.cache_dir.to_string_lossy().contains(".thru"));
469 }
470}