close #12 Implement config commands

This commit is contained in:
fluo10 2025-08-22 07:56:26 +09:00
parent f495af68fc
commit ab05bd10cd
13 changed files with 149 additions and 65 deletions

View file

@ -45,9 +45,9 @@ impl TryFrom<PartialConfig> for Config {
type Error = crate::error::Error; type Error = crate::error::Error;
fn try_from(value: PartialConfig) -> Result<Self, Self::Error> { fn try_from(value: PartialConfig) -> Result<Self, Self::Error> {
Ok(Self{ Ok(Self{
rpc: value.rpc.try_into()?, rpc: value.rpc.ok_or(crate::error::Error::MissingConfig("rpc"))?.try_into()?,
p2p: value.p2p.try_into()?, p2p: value.p2p.ok_or(crate::error::Error::MissingConfig("p2p"))?.try_into()?,
storage: value.storage.try_into()? storage: value.storage.ok_or(crate::error::Error::MissingConfig("storage"))?.try_into()?
}) })
} }
} }
@ -56,19 +56,19 @@ impl TryFrom<PartialConfig> for Config {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct PartialConfig { pub struct PartialConfig {
#[cfg_attr(feature="desktop", command(flatten))] #[cfg_attr(feature="desktop", command(flatten))]
pub p2p: PartialP2pConfig, pub p2p: Option<PartialP2pConfig>,
#[cfg_attr(feature="desktop", command(flatten))] #[cfg_attr(feature="desktop", command(flatten))]
pub storage: PartialStorageConfig, pub storage: Option<PartialStorageConfig>,
#[cfg_attr(feature="desktop", command(flatten))] #[cfg_attr(feature="desktop", command(flatten))]
pub rpc: PartialRpcConfig, pub rpc: Option<PartialRpcConfig>,
} }
impl PartialConfig { impl PartialConfig {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
p2p : PartialP2pConfig::empty().with_new_secret(), p2p : Some(PartialP2pConfig::empty().with_new_private_key()),
storage: PartialStorageConfig::empty(), storage: Some(PartialStorageConfig::empty()),
rpc: PartialRpcConfig::empty(), rpc: Some(PartialRpcConfig::empty()),
} }
} }
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> { pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
@ -90,6 +90,12 @@ impl PartialConfig {
where where
T: AsRef<Path> T: AsRef<Path>
{ {
if !path.as_ref().exists() {
if let Some(x) = path.as_ref().parent() {
std::fs::create_dir_all(x)?;
};
let _ = File::create(&path).await?;
}
let mut file = File::open(path.as_ref()).await?; let mut file = File::open(path.as_ref()).await?;
let mut content = String::new(); let mut content = String::new();
file.read_to_string(&mut content).await?; file.read_to_string(&mut content).await?;
@ -113,9 +119,19 @@ impl PartialConfig {
#[cfg(not(any(target_os="android", target_os="ios")))] #[cfg(not(any(target_os="android", target_os="ios")))]
pub fn default_desktop(app_name: &'static str) -> Self { pub fn default_desktop(app_name: &'static str) -> Self {
Self { Self {
p2p: PartialP2pConfig::default(), p2p: Some(PartialP2pConfig::default()),
rpc: PartialRpcConfig::default(), rpc: Some(PartialRpcConfig::default()),
storage: PartialStorageConfig::default(app_name), storage: Some(PartialStorageConfig::default(app_name)),
}
}
}
impl From<Config> for PartialConfig {
fn from(value: Config) -> Self {
Self {
p2p: Some(value.p2p.into()),
storage: Some(value.storage.into()),
rpc: Some(value.rpc.into())
} }
} }
} }
@ -123,9 +139,9 @@ impl PartialConfig {
impl Emptiable for PartialConfig { impl Emptiable for PartialConfig {
fn empty() -> Self { fn empty() -> Self {
Self { Self {
p2p: PartialP2pConfig::empty(), p2p: None,
storage: PartialStorageConfig::empty(), storage: None,
rpc: PartialRpcConfig::empty() rpc: None,
} }
} }

View file

@ -33,14 +33,14 @@ fn base64_to_keypair(base64: &str) -> Result<Keypair, Error> {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct P2pConfig { pub struct P2pConfig {
pub secret: Keypair, pub private_key: Keypair,
pub listen_ips: Vec<IpAddr>, pub listen_ips: Vec<IpAddr>,
pub port: u16, pub port: u16,
} }
impl P2pConfig { impl P2pConfig {
async fn try_into_swarm (self) -> Result<Swarm<p2p::Behaviour>, Error> { async fn try_into_swarm (self) -> Result<Swarm<p2p::Behaviour>, Error> {
let mut swarm = libp2p::SwarmBuilder::with_existing_identity(self.secret) let mut swarm = libp2p::SwarmBuilder::with_existing_identity(self.private_key)
.with_tokio() .with_tokio()
.with_tcp( .with_tcp(
tcp::Config::default(), tcp::Config::default(),
@ -74,7 +74,7 @@ impl TryFrom<PartialP2pConfig> for P2pConfig {
type Error = crate::error::Error; type Error = crate::error::Error;
fn try_from(raw: PartialP2pConfig) -> Result<P2pConfig, Self::Error> { fn try_from(raw: PartialP2pConfig) -> Result<P2pConfig, Self::Error> {
Ok(P2pConfig { Ok(P2pConfig {
secret: base64_to_keypair(&raw.secret.ok_or(Error::MissingConfig("secret"))?)?, private_key: base64_to_keypair(&raw.private_key.ok_or(Error::MissingConfig("secret"))?)?,
listen_ips: raw.listen_ips.ok_or(Error::MissingConfig("listen_ips"))?, listen_ips: raw.listen_ips.ok_or(Error::MissingConfig("listen_ips"))?,
port: raw.port.ok_or(Error::MissingConfig("port"))? port: raw.port.ok_or(Error::MissingConfig("port"))?
}) })
@ -85,23 +85,26 @@ impl TryFrom<PartialP2pConfig> for P2pConfig {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct PartialP2pConfig { pub struct PartialP2pConfig {
#[cfg_attr(feature="desktop",arg(long))] #[cfg_attr(feature="desktop",arg(long))]
pub secret: Option<String>, pub private_key: Option<String>,
#[cfg_attr(feature="desktop",arg(long))] #[cfg_attr(feature="desktop",arg(long))]
pub listen_ips: Option<Vec<IpAddr>>, pub listen_ips: Option<Vec<IpAddr>>,
#[cfg_attr(feature="desktop",arg(long))] #[cfg_attr(feature="desktop",arg(long))]
pub port: Option<u16>, pub port: Option<u16>,
} }
impl PartialP2pConfig { impl PartialP2pConfig {
pub fn with_new_secret(mut self) -> Self { pub fn with_new_private_key(mut self) -> Self {
self.secret = Some(keypair_to_base64(&Keypair::generate_ed25519())); self.private_key = Some(keypair_to_base64(&Keypair::generate_ed25519()));
self self
} }
pub fn init_private_key(&mut self) {
let _ = self.private_key.insert(keypair_to_base64(&Keypair::generate_ed25519()));
}
} }
impl From<P2pConfig> for PartialP2pConfig { impl From<P2pConfig> for PartialP2pConfig {
fn from(config: P2pConfig) -> Self { fn from(config: P2pConfig) -> Self {
Self { Self {
secret: Some(keypair_to_base64(&config.secret)), private_key: Some(keypair_to_base64(&config.private_key)),
listen_ips: Some(config.listen_ips), listen_ips: Some(config.listen_ips),
port: Some(config.port) port: Some(config.port)
} }
@ -111,7 +114,7 @@ impl From<P2pConfig> for PartialP2pConfig {
impl Default for PartialP2pConfig { impl Default for PartialP2pConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
secret: None, private_key: None,
listen_ips: Some(Vec::from(DEFAULT_P2P_LISTEN_IPS)), listen_ips: Some(Vec::from(DEFAULT_P2P_LISTEN_IPS)),
port: Some(DEFAULT_P2P_PORT), port: Some(DEFAULT_P2P_PORT),
} }
@ -121,21 +124,21 @@ impl Default for PartialP2pConfig {
impl Emptiable for PartialP2pConfig { impl Emptiable for PartialP2pConfig {
fn empty() -> Self { fn empty() -> Self {
Self{ Self{
secret: None, private_key: None,
listen_ips: None, listen_ips: None,
port: None port: None
} }
} }
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
self.secret.is_none() && self.listen_ips.is_none() && self.port.is_none() self.private_key.is_none() && self.listen_ips.is_none() && self.port.is_none()
} }
} }
impl Mergeable for PartialP2pConfig { impl Mergeable for PartialP2pConfig {
fn merge(&mut self, mut other: Self) { fn merge(&mut self, mut other: Self) {
if let Some(x) = other.secret.take() { if let Some(x) = other.private_key.take() {
let _ = self.secret.insert(x); let _ = self.private_key.insert(x);
}; };
if let Some(x) = other.listen_ips.take() { if let Some(x) = other.listen_ips.take() {
let _ = self.listen_ips.insert(x); let _ = self.listen_ips.insert(x);
@ -145,6 +148,20 @@ impl Mergeable for PartialP2pConfig {
}; };
} }
} }
impl Mergeable for Option<PartialP2pConfig> {
fn merge(&mut self, mut other: Self) {
match other.take() {
Some(x) => {
if let Some(y) = self.as_mut() {
y.merge(x);
} else {
let _ = self.insert(x);
}
},
None => {}
};
}
}
#[cfg(test)] #[cfg(test)]

View file

@ -63,4 +63,19 @@ impl Mergeable for PartialRpcConfig {
self.socket_path = Some(x); self.socket_path = Some(x);
} }
} }
}
impl Mergeable for Option<PartialRpcConfig> {
fn merge(&mut self, mut other: Self) {
match other.take() {
Some(x) => {
if let Some(y) = self.as_mut() {
y.merge(x);
} else {
let _ = self.insert(x);
}
},
None => {}
};
}
} }

View file

@ -108,4 +108,19 @@ impl Mergeable for PartialStorageConfig {
let _ = self.cache_directory.insert(x); let _ = self.cache_directory.insert(x);
}; };
} }
}
impl Mergeable for Option<PartialStorageConfig> {
fn merge(&mut self, mut other: Self) {
match other.take() {
Some(x) => {
if let Some(y) = self.as_mut() {
y.merge(x);
} else {
let _ = self.insert(x);
}
},
None => {}
};
}
} }

View file

@ -45,14 +45,14 @@ impl GlobalDatabaseConnections {
where where
T: AsRef<StorageConfig> T: AsRef<StorageConfig>
{ {
config.as_ref().data_directory.join("data.db") config.as_ref().data_directory.join("data.sqlite")
} }
fn get_cache_file_path<T>(config: &T) -> PathBuf fn get_cache_file_path<T>(config: &T) -> PathBuf
where where
T: AsRef<StorageConfig> T: AsRef<StorageConfig>
{ {
config.as_ref().cache_directory.join("cache.db") config.as_ref().cache_directory.join("cache.sqlite")
} }
fn get_url_unchecked<T>(path: T) -> String fn get_url_unchecked<T>(path: T) -> String

View file

@ -14,7 +14,7 @@ pub static TEST_CONFIG: LazyLock<Config> = LazyLock::new(|| {
Config { Config {
p2p: PartialP2pConfig::default().with_new_secret().try_into().unwrap(), p2p: PartialP2pConfig::default().with_new_private_key().try_into().unwrap(),
storage: StorageConfig { storage: StorageConfig {
data_directory: data_dir, data_directory: data_dir,
cache_directory: cache_dir, cache_directory: cache_dir,

View file

@ -2,15 +2,4 @@
pub use caretta_macros::Mergeable; pub use caretta_macros::Mergeable;
pub trait Mergeable: Sized { pub trait Mergeable: Sized {
fn merge(&mut self, other: Self); fn merge(&mut self, other: Self);
}
impl<T> Mergeable for Option<T> {
fn merge(&mut self, mut other: Self) {
match other.take() {
Some(x) => {
let _ = self.insert(x);
},
None => {}
};
}
} }

View file

@ -3,43 +3,67 @@ use std::{net::IpAddr, path::PathBuf, sync::LazyLock};
use clap::Args; use clap::Args;
use caretta_core::{ use caretta_core::{
config::{Config, ConfigError, PartialConfig, PartialP2pConfig, PartialStorageConfig}, config::{Config, ConfigError, PartialConfig, PartialP2pConfig, PartialStorageConfig},
utils::mergeable::Mergeable utils::{emptiable::Emptiable, mergeable::Mergeable}
}; };
use libp2p::identity::Keypair;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell;
#[derive(Args, Clone, Debug)] #[derive(Args, Clone, Debug)]
pub struct ConfigArgs { pub struct ConfigArgs {
#[arg(short = 'c', long = "config")] #[arg(short = 'c', long = "config")]
pub file_path: Option<PathBuf>, pub file_path: Option<PathBuf>,
#[arg(skip)] #[arg(skip)]
pub file_content: Option<PartialConfig>, pub file_content: OnceCell<PartialConfig>,
#[command(flatten)] #[command(flatten)]
pub args: PartialConfig, pub args: PartialConfig,
} }
impl ConfigArgs { impl ConfigArgs {
pub fn get_file_path_or_default(&self, app_name: &'static str) -> PathBuf { fn get_file_path_or_default(&self, app_name: &'static str) -> PathBuf {
self.file_path.clone().unwrap_or( self.file_path.clone().unwrap_or(
dirs::config_local_dir() dirs::config_local_dir()
.unwrap() .expect("Config user directory should be set")
.join(app_name) .join(app_name)
.join(app_name.to_string() + ".conf") .join("config.toml")
) )
} }
pub async fn get_or_read_file_content(&mut self, app_name: &'static str) -> PartialConfig { async fn get_or_read_file_content(&self, app_name: &'static str) -> PartialConfig {
self.file_content.get_or_insert( self.file_content.get_or_init(|| async {
PartialConfig::read_from(self.get_file_path_or_default(app_name)).await.unwrap() PartialConfig::read_from(self.get_file_path_or_default(app_name)).await.expect("Config file should be invalid!")
).clone() }).await.clone()
} }
pub async fn into_config_unchecked(mut self, app_name: &'static str) -> Config { pub async fn to_partial_config_with_default(&self, app_name: &'static str) -> PartialConfig {
let mut default = PartialConfig::default_desktop(app_name); let mut default = PartialConfig::default_desktop(app_name);
let file_content = self.get_or_read_file_content(app_name).await; default.merge(self.to_partial_config_without_default(app_name).await);
let args = self.args; default
default.merge(file_content); }
default.merge(args); pub async fn to_partial_config_without_default(&self, app_name: &'static str) -> PartialConfig {
default.try_into().unwrap() let mut file_content = self.get_or_read_file_content(app_name).await;
let args = self.args.clone();
file_content.merge(args);
file_content
}
async fn has_p2p_private_key(&self, app_name: &'static str) -> bool {
let merged = self.to_partial_config_with_default(app_name).await;
match merged.p2p {
Some(p2p) => p2p.private_key.is_some(),
None => false
}
}
pub async fn into_config(mut self, app_name: &'static str) -> Config {
if !self.has_p2p_private_key(app_name).await {
let path = self.get_file_path_or_default(app_name);
let mut content = self.file_content.get_mut().unwrap();
if let Some(p2p) = content.p2p.as_mut() {
p2p.init_private_key();
} else {
content.p2p.insert(PartialP2pConfig::empty().with_new_private_key());
}
content.write_to(path).await.expect("Config file should be writable first time to initialize secret");
}
self.to_partial_config_with_default(app_name).await.try_into().expect("Some configurations are missing!")
} }
} }

View file

@ -10,6 +10,7 @@ pub struct ConfigCheckCommandArgs{
impl Runnable for ConfigCheckCommandArgs { impl Runnable for ConfigCheckCommandArgs {
async fn run(self, app_name: &'static str) { async fn run(self, app_name: &'static str) {
todo!() let _ = self.config.into_config(app_name).await;
println!("Ok");
} }
} }

View file

@ -1,5 +1,5 @@
use clap::Args; use clap::Args;
use caretta_core::utils::runnable::Runnable; use caretta_core::{config::PartialConfig, utils::runnable::Runnable};
use crate::cli::ConfigArgs; use crate::cli::ConfigArgs;
#[derive(Debug, Args)] #[derive(Debug, Args)]
@ -12,6 +12,12 @@ pub struct ConfigListCommandArgs{
impl Runnable for ConfigListCommandArgs { impl Runnable for ConfigListCommandArgs {
async fn run(self, app_name: &'static str) { async fn run(self, app_name: &'static str) {
todo!() let config: PartialConfig = if self.all {
self.config.into_config(app_name).await.into()
} else {
self.config.to_partial_config_without_default(app_name).await
};
println!("{}", config.into_toml().unwrap())
} }
} }

View file

@ -7,7 +7,7 @@ impl ServerTrait for Server {
where where
T: AsRef<P2pConfig> T: AsRef<P2pConfig>
{ {
let mut swarm = libp2p::SwarmBuilder::with_existing_identity(config.as_ref().secret.clone()) let mut swarm = libp2p::SwarmBuilder::with_existing_identity(config.as_ref().private_key.clone())
.with_tokio() .with_tokio()
.with_tcp( .with_tcp(
tcp::Config::default(), tcp::Config::default(),

View file

@ -11,7 +11,7 @@ pub struct ServerCommandArgs {
} }
impl Runnable for ServerCommandArgs { impl Runnable for ServerCommandArgs {
async fn run(self, app_name: &'static str) { async fn run(self, app_name: &'static str) {
let config = CONFIG.get_or_init::<Config>(self.config.into_config_unchecked(app_name).await).await; let config = CONFIG.get_or_init::<Config>(self.config.into_config(app_name).await).await;
} }
} }

View file

@ -1,3 +1,5 @@
use caretta::utils::runnable::Runnable;
use caretta_example_core::global::APP_NAME;
use clap::Parser; use clap::Parser;
use crate::cli::Cli; use crate::cli::Cli;
@ -8,6 +10,5 @@ mod ipc;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let args = Cli::parse(); let args = Cli::parse();
args.run(APP_NAME).await;
} }