pub use crate::storage::DEFAULT_DATADIR;
use crate::{
chains::{Chain, ChainApi, ChainConf, ChainDef, EthChain},
queryeng::ctx::Ctx,
storage::{StorageApi, StorageConf},
ChainPartitionIndex,
};
use colored::Colorize;
use itertools::Itertools;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc};
use toml::Value;
pub const DEFAULT_CONF_FILE: &str = "config.toml";
#[derive(Debug, Serialize, Deserialize)]
pub struct GlobalConf {
pub app_data_dir: std::path::PathBuf,
#[serde(default)]
pub chains: HashMap<String, Value>,
#[serde(default)]
pub stores: HashMap<String, StorageConf>,
}
impl GlobalConf {
pub async fn init_ctx(&self) -> anyhow::Result<Ctx> {
let ctx = Ctx::new();
let mut chains: Vec<Arc<dyn ChainApi>> = Vec::with_capacity(self.chains.len());
for (id, chain_conf) in &self.chains {
match Chain::try_from_id_empty(id, Some(chain_conf)) {
Ok(mut chain) => {
assert_eq!(chain.name(), id);
let mut explicit_conf = None as Option<&str>;
if let Some(use_conf) = chain_conf.get("use_conf") {
if let Some(s) = use_conf.as_str() {
debug!("using config named: {s} for chain {id}");
explicit_conf = Some(s);
}
}
let (store_conf, conf_name) = match explicit_conf {
Some(n) => (self.stores.get(n), n),
None => (self.stores.get(chain.name()), chain.name()),
};
if let Some(store_conf) = store_conf {
info!("found storage conf {store_conf:?}");
match StorageApi::<ChainPartitionIndex>::try_new(store_conf.to_owned())
.await
{
Ok(store) => {
ctx.add_storage_conf(conf_name, store_conf).await.ok();
let maybe_idx = store.load().await?;
if let Some(idx) = maybe_idx {
info!("loaded partition index from conf {conf_name}");
chain.set_partition_index(idx);
};
}
Err(err) => {
println!(
"{}",
format!(
"failed to initialize storage config {}: {err}",
conf_name.cyan(),
)
.yellow()
)
}
}
} else {
info!("no storage conf found for {id}");
}
chains.push(Arc::from(chain));
}
Err(_) => {
println!(
"{}",
format!("Failed to initialize chain: {}", id.cyan()).yellow()
);
}
}
}
let names = chains.iter().map(|c| c.name()).collect_vec();
info!("loaded chains {names:?}");
if !names.contains(&"eth") {
debug!("no eth chain in users conf file. registering an empty one");
chains.push(Arc::new(EthChain::new(ChainConf {
partition_index: None,
data_fetch_conf: None,
})));
}
for c in chains {
ctx.register_chain(c);
}
Ok(ctx)
}
}
pub const DEFAULT_INDEX_NAME: &str = "eth_mapping.db";
impl Default for GlobalConf {
fn default() -> Self {
let eth_basic: Value = toml::from_str(
r#"rpc = {url = "http://localhost:8545", batch_size = 100 }"#,
)
.expect("hard coded toml literal should parse");
let app_data_dir = DEFAULT_DATADIR.to_path_buf();
let mut chains = HashMap::new();
let mut stores = HashMap::new();
chains.insert("eth".to_owned(), eth_basic);
stores.insert(
"local_default".to_owned(),
StorageConf::File {
dirpath: DEFAULT_DATADIR.to_path_buf(),
filename: DEFAULT_INDEX_NAME.into(),
},
);
Self {
app_data_dir,
chains,
stores,
}
}
}
pub fn default_example_conf() -> String {
let mut lines: Vec<String> = vec![];
lines.push(
"# app data stored here, also default location for locally stored chain data".to_string(),
);
lines.push(format!(
"app_data_dir=\"{}\"",
DEFAULT_DATADIR.to_str().unwrap().to_owned()
));
lines.push("############# configs for building chain data mappings #############".to_string());
lines.push("# chain configs for interacting w/ chain...".to_string());
lines.push("# chain configs are needed when building data maps".to_string());
lines.push("[chains.eth]".to_string());
lines.push(r#"rpc = { url="http://localhost:8545", batch_size = 100 }"#.to_string());
lines.push(
"# stores are used to define a persistence layer for chain data maps you build".to_string(),
);
lines.push("# every data map you build has its own store".to_string());
lines.push("[stores.eth]".to_string());
lines.push("type=\"file\"".to_owned());
lines.push(format!(
"dirpath=\"{}\"",
DEFAULT_DATADIR.to_str().unwrap().to_owned()
));
lines.push(format!("filename=\"{DEFAULT_INDEX_NAME}\""));
lines.join("\n")
}
#[cfg(test)]
mod tests {
use crate::{
chains::test::{ErrorChain, TestChain},
storage::Location,
test::TestDir,
};
use super::*;
#[test]
fn test_conf() {
let _config: GlobalConf = toml::from_str(
r#"
app_data_dir = "./testy"
[chains]
[chains.eth]
rpc_url = "http://localhost:8545"
rpc_batch_size = 100
[stores.local1]
type = "file"
filename="testy.json"
"#,
)
.unwrap();
}
#[test]
fn test_example_default() {
let rawtoml = default_example_conf();
println!("{rawtoml}");
let parsed: GlobalConf =
toml::from_str(&rawtoml).expect("example default is invalid conf!!");
assert_eq!(parsed.app_data_dir, DEFAULT_DATADIR.to_path_buf());
}
#[tokio::test]
async fn test_init_ctx() {
let datadir = TestDir::new(true);
let conf = GlobalConf {
app_data_dir: datadir.path.to_owned(),
chains: toml::from_str(&format!(
"\n[{}] \
\n# empty conf \
\n[{}] \
\n# empty
",
TestChain::ID,
ErrorChain::ID
))
.unwrap(),
stores: toml::from_str(&format!(
"\n[{}] \
\ntype = \"memory\"\
\nbucket = \"test\"\
\n[{}] \
\ntype = \"memory\" \
\nbucket = \"test2\" \
",
TestChain::ID,
ErrorChain::ID
))
.unwrap(),
};
let ctx = conf.init_ctx().await.unwrap();
ctx.catalog().get_chain(TestChain::ID).unwrap();
ctx.catalog().get_chain(ErrorChain::ID).unwrap();
ctx.chain_store_for_loc(&Location::new("memory", Some("test"), "/"))
.await
.unwrap();
ctx.chain_store_for_loc(&Location::new("memory", Some("test2"), "/"))
.await
.unwrap();
ctx.chain_store_for_loc(&Location::new("memory", Some("badbucket"), "/"))
.await
.unwrap_err();
}
}