Skip to main content

abi_loader/fetcher/
mod.rs

1//! Import Fetcher Infrastructure
2//!
3//! This module provides a pluggable fetcher system for resolving ABI imports
4//! from various sources: local paths, git repositories, HTTP URLs, and on-chain.
5
6#[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/* ============================================================================
18Fetcher Configuration
19============================================================================ */
20
21/* Configuration for which import types are allowed */
22#[derive(Debug, Clone)]
23pub struct FetcherConfig {
24    /* Allow local path imports */
25    pub allow_path: bool,
26    /* Allow git repository imports */
27    pub allow_git: bool,
28    /* Allow HTTP/HTTPS URL imports */
29    pub allow_http: bool,
30    /* Allow on-chain imports */
31    pub allow_onchain: bool,
32
33    /* Git-specific configuration */
34    pub git_config: GitFetcherConfig,
35
36    /* On-chain specific configuration */
37    pub onchain_config: OnchainFetcherConfig,
38
39    /* Caching configuration */
40    pub cache_config: CacheConfig,
41}
42
43impl Default for FetcherConfig {
44    fn default() -> Self {
45        Self::cli_default()
46    }
47}
48
49impl FetcherConfig {
50    /* Default configuration for CLI usage - all import types allowed */
51    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    /* Configuration for WASM runtime - no remote fetching */
64    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    /* Configuration for production builds - only on-chain allowed */
77    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    /* Configuration for local development - only path imports */
90    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    /* Check if a given import source is allowed by this configuration */
103    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/* Git fetcher configuration */
114#[derive(Debug, Clone, Default)]
115pub struct GitFetcherConfig {
116    /* Path to SSH key for authentication (optional, uses ssh-agent by default) */
117    pub ssh_key_path: Option<PathBuf>,
118    /* Use git credential helper for HTTPS auth */
119    pub use_credential_helper: bool,
120    /* HTTP/HTTPS proxy URL */
121    pub proxy: Option<String>,
122    /* Timeout for git operations in seconds */
123    pub timeout_seconds: u64,
124}
125
126impl GitFetcherConfig {
127    /* Create with default timeout */
128    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/* On-chain fetcher configuration */
139#[derive(Debug, Clone)]
140pub struct OnchainFetcherConfig {
141    /* Map of network name to RPC endpoint URL */
142    pub rpc_endpoints: std::collections::HashMap<String, String>,
143    /* Default network to use if not specified in import */
144    pub default_network: String,
145    /* Timeout for RPC calls in seconds */
146    pub timeout_seconds: u64,
147    /* ABI manager program public key (Thru address) */
148    pub abi_manager_program_id: String,
149    /* Whether ABI manager accounts are ephemeral */
150    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    /* Get the RPC endpoint for a given network */
177    pub fn get_endpoint(&self, network: &str) -> Option<&str> {
178        self.rpc_endpoints.get(network).map(|s| s.as_str())
179    }
180
181    /* Add or update an RPC endpoint */
182    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/* Cache configuration */
188#[derive(Debug, Clone)]
189pub struct CacheConfig {
190    /* Enable caching */
191    pub enabled: bool,
192    /* Directory for cached imports */
193    pub cache_dir: PathBuf,
194    /* Maximum age of cached items in seconds (0 = no expiry) */
195    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, /* 1 hour */
204        }
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    /* Create a disabled cache configuration */
223    pub fn disabled() -> Self {
224        Self {
225            enabled: false,
226            cache_dir: PathBuf::new(),
227            max_age_seconds: 0,
228        }
229    }
230
231    /* Create with custom cache directory */
232    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/* ============================================================================
242Fetch Context
243============================================================================ */
244
245/* Context passed to fetchers during resolution */
246#[derive(Debug, Clone)]
247pub struct FetchContext {
248    /* Base path for resolving relative path imports */
249    pub base_path: Option<PathBuf>,
250    /* True if the parent import was from a remote source */
251    pub parent_is_remote: bool,
252    /* Include directories for path resolution */
253    pub include_dirs: Vec<PathBuf>,
254}
255
256impl FetchContext {
257    /* Create a new fetch context for a root file */
258    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    /* Create a child context for an import from this context */
267    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/* ============================================================================
277Fetch Result
278============================================================================ */
279
280/* Result of successfully fetching an ABI file */
281#[derive(Debug, Clone)]
282pub struct FetchResult {
283    /* Raw YAML content of the ABI file */
284    pub content: String,
285    /* Canonical location identifier (for caching and cycle detection) */
286    pub canonical_location: String,
287    /* Whether the source is remote (git, http, onchain) */
288    pub is_remote: bool,
289    /* Resolved file path (for path imports only) */
290    pub resolved_path: Option<PathBuf>,
291}
292
293/* ============================================================================
294Fetch Error
295============================================================================ */
296
297/* Errors that can occur during fetching */
298#[derive(Debug)]
299pub enum FetchError {
300    /* Import source type not supported by this fetcher */
301    UnsupportedSource(String),
302    /* Import source type not allowed by configuration */
303    NotAllowed(ImportSource),
304    /* Local import from remote parent not allowed */
305    LocalFromRemote(String),
306    /* File not found */
307    NotFound(String),
308    /* IO error */
309    Io(std::io::Error),
310    /* Git operation failed */
311    Git(String),
312    /* HTTP request failed */
313    Http { status: u16, message: String },
314    /* On-chain fetch failed */
315    Onchain(String),
316    /* Parse error */
317    Parse(String),
318    /* Network not configured */
319    UnknownNetwork(String),
320    /* Revision mismatch */
321    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
367/* ============================================================================
368Fetcher Trait
369============================================================================ */
370
371/* Trait for import source fetchers */
372pub trait ImportFetcher: Send + Sync {
373    /* Check if this fetcher handles the given import source type */
374    fn handles(&self, source: &ImportSource) -> bool;
375
376    /* Fetch the ABI content from the source */
377    fn fetch(&self, source: &ImportSource, ctx: &FetchContext) -> Result<FetchResult, FetchError>;
378}
379
380/* ============================================================================
381Composite Fetcher
382============================================================================ */
383
384/* Composite fetcher that delegates to the appropriate backend */
385pub struct CompositeFetcher {
386    fetchers: Vec<Box<dyn ImportFetcher>>,
387    config: FetcherConfig,
388}
389
390impl CompositeFetcher {
391    /* Create a new composite fetcher with the given configuration */
392    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    /* Fetch an import source */
417    pub fn fetch(
418        &self,
419        source: &ImportSource,
420        ctx: &FetchContext,
421    ) -> Result<FetchResult, FetchError> {
422        /* Check if source type is allowed */
423        if !self.config.is_allowed(source) {
424            return Err(FetchError::NotAllowed(source.clone()));
425        }
426
427        /* Find appropriate fetcher */
428        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    /* Get the configuration */
438    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}