#[cfg(not(target_arch = "wasm32"))]
pub mod git;
#[cfg(not(target_arch = "wasm32"))]
pub mod http;
#[cfg(not(target_arch = "wasm32"))]
pub mod onchain;
pub mod path;
use crate::file::ImportSource;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct FetcherConfig {
pub allow_path: bool,
pub allow_git: bool,
pub allow_http: bool,
pub allow_onchain: bool,
pub git_config: GitFetcherConfig,
pub onchain_config: OnchainFetcherConfig,
pub cache_config: CacheConfig,
}
impl Default for FetcherConfig {
fn default() -> Self {
Self::cli_default()
}
}
impl FetcherConfig {
pub fn cli_default() -> Self {
Self {
allow_path: true,
allow_git: true,
allow_http: true,
allow_onchain: true,
git_config: GitFetcherConfig::default(),
onchain_config: OnchainFetcherConfig::default(),
cache_config: CacheConfig::default(),
}
}
pub fn wasm_default() -> Self {
Self {
allow_path: false,
allow_git: false,
allow_http: false,
allow_onchain: false,
git_config: GitFetcherConfig::default(),
onchain_config: OnchainFetcherConfig::default(),
cache_config: CacheConfig::disabled(),
}
}
pub fn production_build() -> Self {
Self {
allow_path: false,
allow_git: false,
allow_http: false,
allow_onchain: true,
git_config: GitFetcherConfig::default(),
onchain_config: OnchainFetcherConfig::default(),
cache_config: CacheConfig::default(),
}
}
pub fn local_only() -> Self {
Self {
allow_path: true,
allow_git: false,
allow_http: false,
allow_onchain: false,
git_config: GitFetcherConfig::default(),
onchain_config: OnchainFetcherConfig::default(),
cache_config: CacheConfig::disabled(),
}
}
pub fn is_allowed(&self, source: &ImportSource) -> bool {
match source {
ImportSource::Path { .. } => self.allow_path,
ImportSource::Git { .. } => self.allow_git,
ImportSource::Http { .. } => self.allow_http,
ImportSource::Onchain { .. } => self.allow_onchain,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct GitFetcherConfig {
pub ssh_key_path: Option<PathBuf>,
pub use_credential_helper: bool,
pub proxy: Option<String>,
pub timeout_seconds: u64,
}
impl GitFetcherConfig {
pub fn new() -> Self {
Self {
ssh_key_path: None,
use_credential_helper: true,
proxy: None,
timeout_seconds: 60,
}
}
}
#[derive(Debug, Clone)]
pub struct OnchainFetcherConfig {
pub rpc_endpoints: std::collections::HashMap<String, String>,
pub default_network: String,
pub timeout_seconds: u64,
pub abi_manager_program_id: String,
pub abi_manager_is_ephemeral: bool,
}
impl Default for OnchainFetcherConfig {
fn default() -> Self {
let mut rpc_endpoints = std::collections::HashMap::new();
rpc_endpoints.insert(
"mainnet".to_string(),
"https://rpc.thru.network".to_string(),
);
rpc_endpoints.insert(
"testnet".to_string(),
"https://rpc-testnet.thru.network".to_string(),
);
Self {
rpc_endpoints,
default_network: "mainnet".to_string(),
timeout_seconds: 30,
abi_manager_program_id: "taAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACrG7".to_string(),
abi_manager_is_ephemeral: false,
}
}
}
impl OnchainFetcherConfig {
pub fn get_endpoint(&self, network: &str) -> Option<&str> {
self.rpc_endpoints.get(network).map(|s| s.as_str())
}
pub fn set_endpoint(&mut self, network: impl Into<String>, endpoint: impl Into<String>) {
self.rpc_endpoints.insert(network.into(), endpoint.into());
}
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub enabled: bool,
pub cache_dir: PathBuf,
pub max_age_seconds: u64,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
enabled: true,
cache_dir: default_cache_dir(),
max_age_seconds: 3600,
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn default_cache_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".thru")
.join("abi-cache")
}
#[cfg(target_arch = "wasm32")]
fn default_cache_dir() -> PathBuf {
PathBuf::new()
}
impl CacheConfig {
pub fn disabled() -> Self {
Self {
enabled: false,
cache_dir: PathBuf::new(),
max_age_seconds: 0,
}
}
pub fn with_dir(cache_dir: PathBuf) -> Self {
Self {
enabled: true,
cache_dir,
max_age_seconds: 3600,
}
}
}
#[derive(Debug, Clone)]
pub struct FetchContext {
pub base_path: Option<PathBuf>,
pub parent_is_remote: bool,
pub include_dirs: Vec<PathBuf>,
}
impl FetchContext {
pub fn for_root(file_path: Option<PathBuf>, include_dirs: Vec<PathBuf>) -> Self {
Self {
base_path: file_path,
parent_is_remote: false,
include_dirs,
}
}
pub fn child_context(&self, source: &ImportSource, resolved_path: Option<PathBuf>) -> Self {
Self {
base_path: resolved_path,
parent_is_remote: source.is_remote(),
include_dirs: self.include_dirs.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct FetchResult {
pub content: String,
pub canonical_location: String,
pub is_remote: bool,
pub resolved_path: Option<PathBuf>,
}
#[derive(Debug)]
pub enum FetchError {
UnsupportedSource(String),
NotAllowed(ImportSource),
LocalFromRemote(String),
NotFound(String),
Io(std::io::Error),
Git(String),
Http { status: u16, message: String },
Onchain(String),
Parse(String),
UnknownNetwork(String),
RevisionMismatch { required: String, actual: u64 },
}
impl std::fmt::Display for FetchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FetchError::UnsupportedSource(s) => write!(f, "Unsupported import source: {}", s),
FetchError::NotAllowed(s) => write!(f, "Import type not allowed: {:?}", s),
FetchError::LocalFromRemote(s) => {
write!(f, "Local import '{}' not allowed from remote source", s)
}
FetchError::NotFound(s) => write!(f, "Import not found: {}", s),
FetchError::Io(e) => write!(f, "IO error: {}", e),
FetchError::Git(s) => write!(f, "Git error: {}", s),
FetchError::Http { status, message } => {
write!(f, "HTTP error {}: {}", status, message)
}
FetchError::Onchain(s) => write!(f, "On-chain fetch error: {}", s),
FetchError::Parse(s) => write!(f, "Parse error: {}", s),
FetchError::UnknownNetwork(s) => write!(f, "Unknown network: {}", s),
FetchError::RevisionMismatch { required, actual } => {
write!(
f,
"Revision mismatch: required {}, got {}",
required, actual
)
}
}
}
}
impl std::error::Error for FetchError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FetchError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for FetchError {
fn from(e: std::io::Error) -> Self {
FetchError::Io(e)
}
}
pub trait ImportFetcher: Send + Sync {
fn handles(&self, source: &ImportSource) -> bool;
fn fetch(&self, source: &ImportSource, ctx: &FetchContext) -> Result<FetchResult, FetchError>;
}
pub struct CompositeFetcher {
fetchers: Vec<Box<dyn ImportFetcher>>,
config: FetcherConfig,
}
impl CompositeFetcher {
pub fn new(config: FetcherConfig) -> Result<Self, FetchError> {
let mut fetchers: Vec<Box<dyn ImportFetcher>> = Vec::new();
if config.allow_path {
fetchers.push(Box::new(path::PathFetcher::new()));
}
#[cfg(not(target_arch = "wasm32"))]
if config.allow_git {
fetchers.push(Box::new(git::GitFetcher::new(&config.git_config)));
}
#[cfg(not(target_arch = "wasm32"))]
if config.allow_http {
fetchers.push(Box::new(http::HttpFetcher::new()?));
}
#[cfg(not(target_arch = "wasm32"))]
if config.allow_onchain {
fetchers.push(Box::new(onchain::OnchainFetcher::new(
&config.onchain_config,
)));
}
Ok(Self { fetchers, config })
}
pub fn fetch(
&self,
source: &ImportSource,
ctx: &FetchContext,
) -> Result<FetchResult, FetchError> {
if !self.config.is_allowed(source) {
return Err(FetchError::NotAllowed(source.clone()));
}
for fetcher in &self.fetchers {
if fetcher.handles(source) {
return fetcher.fetch(source, ctx);
}
}
Err(FetchError::UnsupportedSource(format!("{:?}", source)))
}
pub fn config(&self) -> &FetcherConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fetcher_config_is_allowed() {
let config = FetcherConfig::local_only();
let path_import = ImportSource::Path {
path: "test.abi.yaml".to_string(),
};
let git_import = ImportSource::Git {
url: "https://github.com/test/repo".to_string(),
git_ref: "main".to_string(),
path: "abi.yaml".to_string(),
};
assert!(config.is_allowed(&path_import));
assert!(!config.is_allowed(&git_import));
}
#[test]
fn test_cache_config_default() {
let config = CacheConfig::default();
assert!(config.enabled);
assert!(config.cache_dir.to_string_lossy().contains(".thru"));
}
}