diff --git a/parser_config.json b/parser_config.json new file mode 100644 index 0000000..2aecdce --- /dev/null +++ b/parser_config.json @@ -0,0 +1,5 @@ +{ + "sierra_extension": ".contract_class.json", + "contract_aliases": {}, + "type_aliases": {} +} diff --git a/src/bin/cli/args.rs b/src/bin/cli/args.rs index 35cdc05..285a44b 100644 --- a/src/bin/cli/args.rs +++ b/src/bin/cli/args.rs @@ -25,6 +25,11 @@ pub struct CainomeArgs { )] pub artifacts_path: Option, + #[arg(long)] + #[arg(value_name = "PATH")] + #[arg(help = "Path of a JSON file defining Cainome parsing configuration.")] + pub parser_config: Option, + #[arg(long)] #[arg(value_name = "ADDRESS")] #[arg(conflicts_with = "artifacts_path")] diff --git a/src/bin/cli/contract/mod.rs b/src/bin/cli/contract/mod.rs index 6e9ed45..aa49196 100644 --- a/src/bin/cli/contract/mod.rs +++ b/src/bin/cli/contract/mod.rs @@ -1,12 +1,10 @@ use cainome_parser::{AbiParser, TokenizedAbi}; use camino::Utf8PathBuf; -use convert_case::{Case, Casing}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use url::Url; -// use starknet::core::types::contract::*; - use starknet::{ core::types::{BlockId, BlockTag, ContractClass, FieldElement}, providers::{jsonrpc::HttpTransport, AnyProvider, JsonRpcClient, Provider}, @@ -14,8 +12,6 @@ use starknet::{ use crate::error::{CainomeCliResult, Error}; -const SIERRA_EXT: &str = ".contract_class.json"; - #[derive(Debug)] pub enum ContractOrigin { /// Contract's ABI was loaded from a local Sierra class file @@ -35,10 +31,41 @@ pub struct ContractData { pub tokens: TokenizedAbi, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContractParserConfig { + /// The file extension that should be considered as a Sierra file. + pub sierra_extension: String, + /// The type aliases to be provided to the Cainome parser. + pub type_aliases: HashMap, + /// The contract aliases to be provided to the Cainome parser. + pub contract_aliases: HashMap, +} + +impl ContractParserConfig { + pub fn from_json(path: &Utf8PathBuf) -> CainomeCliResult { + Ok(serde_json::from_reader(std::io::BufReader::new( + std::fs::File::open(path)?, + ))?) + } +} + +impl Default for ContractParserConfig { + fn default() -> Self { + Self { + sierra_extension: ".contract_class.json".to_string(), + type_aliases: HashMap::default(), + contract_aliases: HashMap::default(), + } + } +} + pub struct ContractParser {} impl ContractParser { - pub fn from_artifacts_path(path: Utf8PathBuf) -> CainomeCliResult> { + pub fn from_artifacts_path( + path: Utf8PathBuf, + config: &ContractParserConfig, + ) -> CainomeCliResult> { let mut contracts = vec![]; for entry in fs::read_dir(path)? { @@ -47,23 +74,25 @@ impl ContractParser { if path.is_file() { if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { - if !file_name.ends_with(SIERRA_EXT) { + if !file_name.ends_with(&config.sierra_extension) { continue; } let file_content = fs::read_to_string(&path)?; - // TODO: check how the aliases can be passed to the CLI....! - // It's a simple HashMap, flat file with two columns - // may be ok? - let aliases = HashMap::new(); - - match AbiParser::tokens_from_abi_string(&file_content, &aliases) { + match AbiParser::tokens_from_abi_string(&file_content, &config.type_aliases) { Ok(tokens) => { - let contract_name = file_name - .trim_end_matches(SIERRA_EXT) - .from_case(Case::Snake) - .to_case(Case::Pascal); + let contract_name = { + let n = file_name.trim_end_matches(&config.sierra_extension); + if let Some(alias) = config.contract_aliases.get(n) { + tracing::trace!( + "Aliasing {file_name} contract name with {alias}" + ); + alias + } else { + n + } + }; tracing::trace!( "Adding {contract_name} ({file_name}) to the list of contracts" @@ -89,6 +118,7 @@ impl ContractParser { name: &str, address: FieldElement, rpc_url: Url, + type_aliases: &HashMap, ) -> CainomeCliResult { let provider = AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new(rpc_url))); @@ -96,14 +126,9 @@ impl ContractParser { .get_class_at(BlockId::Tag(BlockTag::Latest), address) .await?; - // TODO: check how the aliases can be passed to the CLI....! - // It's a simple HashMap, flat file with two columns - // may be ok? - let aliases = HashMap::new(); - match class { ContractClass::Sierra(sierra) => { - match AbiParser::tokens_from_abi_string(&sierra.abi, &aliases) { + match AbiParser::tokens_from_abi_string(&sierra.abi, type_aliases) { Ok(tokens) => Ok(ContractData { name: name.to_string(), origin: ContractOrigin::FetchedFromChain(address), diff --git a/src/bin/cli/main.rs b/src/bin/cli/main.rs index 86442e7..408c9a4 100644 --- a/src/bin/cli/main.rs +++ b/src/bin/cli/main.rs @@ -7,32 +7,49 @@ mod error; mod plugins; use args::CainomeArgs; -use contract::ContractParser; +use contract::{ContractParser, ContractParserConfig}; +use error::{CainomeCliResult, Error}; use plugins::{PluginInput, PluginManager}; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> CainomeCliResult<()> { init_logging()?; - let config = CainomeArgs::parse(); - tracing::trace!("config: {:?}", config); - - let contracts = if let Some(path) = config.artifacts_path { - ContractParser::from_artifacts_path(path)? - } else if let (Some(name), Some(address), Some(url)) = ( - config.contract_name, - config.contract_address, - config.rpc_url, - ) { - vec![ContractParser::from_chain(&name, address, url).await?] + let args = CainomeArgs::parse(); + tracing::trace!("args: {:?}", args); + + let parser_config = if let Some(path) = args.parser_config { + ContractParserConfig::from_json(&path)? + } else { + ContractParserConfig::default() + }; + + let contracts = if let Some(path) = args.artifacts_path { + let ret = ContractParser::from_artifacts_path(path.clone(), &parser_config)?; + + if ret.is_empty() { + tracing::error!( + "No contract found with extension '{}' into '{}' directory", + parser_config.sierra_extension, + path + ); + + return Err(Error::Other("Invalid arguments".to_string())); + } + + ret + } else if let (Some(name), Some(address), Some(url)) = + (args.contract_name, args.contract_address, args.rpc_url) + { + vec![ContractParser::from_chain(&name, address, url, &parser_config.type_aliases).await?] } else { panic!("Invalid arguments: no contracts to be parsed"); }; - let pm = PluginManager::from(config.plugins); + let pm = PluginManager::from(args.plugins); pm.generate(PluginInput { - output_dir: config.output_dir, + output_dir: args.output_dir, contracts, }) .await?; @@ -40,12 +57,15 @@ async fn main() -> Result<(), Box> { Ok(()) } -pub fn init_logging() -> Result<(), Box> { +pub fn init_logging() -> CainomeCliResult<()> { const DEFAULT_LOG_FILTER: &str = "info,cainome=trace"; let builder = fmt::Subscriber::builder().with_env_filter( - EnvFilter::try_from_default_env().or(EnvFilter::try_new(DEFAULT_LOG_FILTER))?, + EnvFilter::try_from_default_env() + .or(EnvFilter::try_new(DEFAULT_LOG_FILTER)) + .map_err(|e| Error::Other(format!("Tracing error: {:?}", e)))?, ); - Ok(tracing::subscriber::set_global_default(builder.finish())?) + tracing::subscriber::set_global_default(builder.finish()) + .map_err(|e| Error::Other(format!("Tracing error: {:?}", e))) } diff --git a/src/bin/cli/plugins/builtins/rust.rs b/src/bin/cli/plugins/builtins/rust.rs index 1f0384b..de465e4 100644 --- a/src/bin/cli/plugins/builtins/rust.rs +++ b/src/bin/cli/plugins/builtins/rust.rs @@ -1,5 +1,6 @@ use async_trait::async_trait; use cainome_rs::{self}; +use convert_case::{Case, Casing}; use crate::error::CainomeCliResult; use crate::plugins::builtins::BuiltinPlugin; @@ -16,11 +17,26 @@ impl RustPlugin { #[async_trait] impl BuiltinPlugin for RustPlugin { async fn generate_code(&self, input: &PluginInput) -> CainomeCliResult<()> { - tracing::trace!("Rust plugin requested:\n{:?}\n", input); + tracing::trace!("Rust plugin requested"); for contract in &input.contracts { - let expanded = cainome_rs::abi_to_tokenstream(&contract.name, &contract.tokens); - let filename = format!("{}.rs", contract.name); + // The contract name contains the fully qualified path of the cairo module. + // For now, let's only take the latest part of this path. + // TODO: if a project has several contracts with the same name under different + // namespaces, we should provide a solution to solve those conflicts. + let contract_name = contract + .name + .split("::") + .last() + .unwrap_or(&contract.name) + .from_case(Case::Snake) + .to_case(Case::Pascal); + + let expanded = cainome_rs::abi_to_tokenstream(&contract_name, &contract.tokens); + let filename = format!( + "{}.rs", + contract_name.from_case(Case::Pascal).to_case(Case::Snake) + ); let mut out_path = input.output_dir.clone(); out_path.push(filename);