subrpc_core/
local_data.rs

1use anyhow::Result;
2use chrono::{DateTime, Local};
3use log::*;
4use serde::{Deserialize, Serialize};
5use std::{
6	collections::{HashMap, HashSet},
7	fs::{self, File},
8	io::{Read, Write},
9	path::{Path, PathBuf},
10};
11
12use crate::{endpoint::Endpoint, Registry};
13
14/// Local user data collected from the various regitries.
15///
16/// It contains the list of registries. Some may be disabled.
17/// The data for each registry can be updated in order to keep
18/// a fresh list of endpoints.
19#[derive(PartialEq, Debug, Deserialize, Serialize)]
20pub struct LocalData {
21	/// File where the local data are stored
22	pub file: PathBuf,
23
24	/// List of the registries where the RPC endpoints are pulled from
25	pub registries: HashMap<String, Registry>,
26
27	/// DateTime of the last update of the data
28	pub last_update: Option<DateTime<Local>>,
29}
30
31impl LocalData {
32	pub fn get_default_file() -> PathBuf {
33		let home = dirs::home_dir().expect("Failed fetching home dir");
34		let dir = Path::new(&home).join(".subrpc");
35		let _ = fs::create_dir_all(&dir);
36		dir.join("data.json")
37	}
38
39	/// Returns true if the local file exists
40	pub fn initialized(&self) -> bool {
41		self.file.exists()
42	}
43
44	/// Initialize a DB based on a given file.
45	/// After initializing a DB, you should ensure it contains
46	/// at least one registry and call the [Self::refresh] function.
47	pub fn init(file: &Path, force: bool) -> Result<Self> {
48		debug!("Initializing local data in {} with force: {:?}", file.display(), force);
49
50		let data = Self { file: file.to_path_buf(), ..Default::default() };
51
52		if file.exists() && !force {
53			info!("File already exists: {}", file.display());
54			data.load()
55		} else {
56			data.save()
57		}
58	}
59
60	/// Load [LocalData] and deserialize data from [file].
61	pub fn load(self) -> Result<Self> {
62		debug!("Loading data from {}", self.file.display());
63		let mut fs = File::open(self.file)?;
64		let mut s = String::new();
65		fs.read_to_string(&mut s)?;
66		serde_json::from_str(&s).map_err(anyhow::Error::msg)
67	}
68
69	/// Loops through each registry, each network/chain, each endpoint
70	/// and update the endpoints lists.
71	pub fn refresh(mut self) -> Self {
72		debug!("Refreshing registries");
73
74		self.registries.iter_mut().for_each(|(_registry_name, reg)| {
75			debug!(" - {} - enabled: {:?}", &reg.name, &reg.enabled);
76			// println!("reg = {:?}", &reg);
77			match reg.update() {
78				Ok(_) => {
79					info!("Update of '{}' OK", reg.name);
80				}
81				Err(e) => {
82					// eprintln!("{e:?}");
83					error!("Update registry '{}' failed: {e:?}", reg.name);
84				}
85			}
86		});
87
88		self.last_update = Some(Local::now());
89		self
90	}
91
92	/// Add a new registry. Registries are identitfied by their names, make sure the name is unique.
93	pub fn add_registry(mut self, registry: Registry) -> Self {
94		self.registries.insert(registry.name.clone(), registry);
95		self
96	}
97
98	/// Save the current state to file
99	pub fn save(self) -> Result<Self> {
100		debug!("Saving data to {}", self.file.display());
101		let json = serde_json::to_string_pretty(&self)?;
102		let mut fs = File::create(&self.file)?;
103		fs.write_all(json.as_bytes())?;
104		Ok(self)
105	}
106
107	/// Get a list of endpoints matching an optional filter. If not
108	/// `chain` filter is passed, all endpoints are returned.
109	pub fn get_endpoints(&self, chain: Option<&str>) -> HashSet<Endpoint> {
110		let mut endpoint_vec: HashSet<Endpoint> = HashSet::new();
111		self.registries.iter().for_each(|(_, reg)| {
112			if !reg.enabled {
113				// skipping
114			} else {
115				reg.rpc_endpoints
116					.iter()
117					.filter(|(c, _)| {
118						if let Some(filter) = chain {
119							c.as_str().to_ascii_lowercase() == filter.to_ascii_lowercase()
120						} else {
121							true
122						}
123					})
124					.for_each(|(_, e)| {
125						let ee = e.clone();
126						endpoint_vec.extend(ee);
127					});
128			}
129		});
130		endpoint_vec
131	}
132
133	/// Print the list of registries.
134	///
135	/// See also [Self::print_summary].
136	pub fn print_registries(&self) {
137		// println!("self.registries = {:?}", self.registries);
138		self.registries.iter().for_each(|(_name, reg)| {
139			println!("- [{}] {:?} {:?}", if reg.enabled { "X" } else { " " }, reg.name, reg.url);
140		})
141	}
142
143	/// Print a summary of your local db. It shows more information than [Self::print_registries].
144	pub fn print_summary(&self) {
145		self.registries.iter().for_each(|(_name, reg)| {
146			println!(
147				"- [{}] {} - {}",
148				if reg.enabled { "X" } else { " " },
149				reg.name,
150				if let Some(url) = &reg.url { url } else { "n/a" }
151			);
152			println!("      rpc endpoints: {:?}", reg.rpc_endpoints.len());
153			println!("      last update: {:?}", reg.last_update);
154		})
155	}
156}
157
158impl Default for LocalData {
159	fn default() -> Self {
160		Self { file: Self::get_default_file(), registries: HashMap::new(), last_update: None }
161	}
162}
163
164#[cfg(test)]
165mod test_local_data {
166	use super::*;
167
168	#[test]
169	fn test_builder() {
170		let data = LocalData::init(&LocalData::get_default_file(), true)
171            .expect("Forced init should work")
172            .save()
173            .expect("Saving data should work")
174            .load().expect("Load works")
175            .add_registry(Registry::new("SubRPC", "https://raw.githubusercontent.com/chevdor/subrpc/master/registry/subrpc.json"))
176            .add_registry(Registry::new("SubRPC Gist", "https://gist.githubusercontent.com/chevdor/a8b381911c28f6de02dde62ed1a17dec/raw/6992b0a2924f80f691e4844c1731564f0e2a62ec/data.json"))
177            .refresh()
178            .save().expect("Saving works");
179		println!("{data:#?}");
180	}
181
182	#[test]
183	fn test_merge() {
184		let data = LocalData::init(&LocalData::get_default_file(), true)
185            .expect("Forced init should work")
186            .add_registry(Registry::new("SubRPC Gist 1", "https://gist.githubusercontent.com/chevdor/a8b381911c28f6de02dde62ed1a17dec/raw/6992b0a2924f80f691e4844c1731564f0e2a62ec/data.json"))
187            .add_registry(Registry::new("SubRPC Gist 2", "https://gist.githubusercontent.com/chevdor/a8b381911c28f6de02dde62ed1a17dec/raw/6992b0a2924f80f691e4844c1731564f0e2a62ec/data2.json"))
188            .refresh()
189            .save().expect("Saving works");
190		assert_eq!(2, data.registries.len());
191		println!("{data:#?}");
192	}
193}