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;
fn try_from(value: PartialConfig) -> Result<Self, Self::Error> {
Ok(Self{
rpc: value.rpc.try_into()?,
p2p: value.p2p.try_into()?,
storage: value.storage.try_into()?
rpc: value.rpc.ok_or(crate::error::Error::MissingConfig("rpc"))?.try_into()?,
p2p: value.p2p.ok_or(crate::error::Error::MissingConfig("p2p"))?.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)]
pub struct PartialConfig {
#[cfg_attr(feature="desktop", command(flatten))]
pub p2p: PartialP2pConfig,
pub p2p: Option<PartialP2pConfig>,
#[cfg_attr(feature="desktop", command(flatten))]
pub storage: PartialStorageConfig,
pub storage: Option<PartialStorageConfig>,
#[cfg_attr(feature="desktop", command(flatten))]
pub rpc: PartialRpcConfig,
pub rpc: Option<PartialRpcConfig>,
}
impl PartialConfig {
pub fn new() -> Self {
Self {
p2p : PartialP2pConfig::empty().with_new_secret(),
storage: PartialStorageConfig::empty(),
rpc: PartialRpcConfig::empty(),
p2p : Some(PartialP2pConfig::empty().with_new_private_key()),
storage: Some(PartialStorageConfig::empty()),
rpc: Some(PartialRpcConfig::empty()),
}
}
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
@ -90,6 +90,12 @@ impl PartialConfig {
where
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 content = String::new();
file.read_to_string(&mut content).await?;
@ -113,9 +119,19 @@ impl PartialConfig {
#[cfg(not(any(target_os="android", target_os="ios")))]
pub fn default_desktop(app_name: &'static str) -> Self {
Self {
p2p: PartialP2pConfig::default(),
rpc: PartialRpcConfig::default(),
storage: PartialStorageConfig::default(app_name),
p2p: Some(PartialP2pConfig::default()),
rpc: Some(PartialRpcConfig::default()),
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 {
fn empty() -> Self {
Self {
p2p: PartialP2pConfig::empty(),
storage: PartialStorageConfig::empty(),
rpc: PartialRpcConfig::empty()
p2p: None,
storage: None,
rpc: None,
}
}

View file

@ -33,14 +33,14 @@ fn base64_to_keypair(base64: &str) -> Result<Keypair, Error> {
#[derive(Clone, Debug)]
pub struct P2pConfig {
pub secret: Keypair,
pub private_key: Keypair,
pub listen_ips: Vec<IpAddr>,
pub port: u16,
}
impl P2pConfig {
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_tcp(
tcp::Config::default(),
@ -74,7 +74,7 @@ impl TryFrom<PartialP2pConfig> for P2pConfig {
type Error = crate::error::Error;
fn try_from(raw: PartialP2pConfig) -> Result<P2pConfig, Self::Error> {
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"))?,
port: raw.port.ok_or(Error::MissingConfig("port"))?
})
@ -85,23 +85,26 @@ impl TryFrom<PartialP2pConfig> for P2pConfig {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct PartialP2pConfig {
#[cfg_attr(feature="desktop",arg(long))]
pub secret: Option<String>,
pub private_key: Option<String>,
#[cfg_attr(feature="desktop",arg(long))]
pub listen_ips: Option<Vec<IpAddr>>,
#[cfg_attr(feature="desktop",arg(long))]
pub port: Option<u16>,
}
impl PartialP2pConfig {
pub fn with_new_secret(mut self) -> Self {
self.secret = Some(keypair_to_base64(&Keypair::generate_ed25519()));
pub fn with_new_private_key(mut self) -> Self {
self.private_key = Some(keypair_to_base64(&Keypair::generate_ed25519()));
self
}
pub fn init_private_key(&mut self) {
let _ = self.private_key.insert(keypair_to_base64(&Keypair::generate_ed25519()));
}
}
impl From<P2pConfig> for PartialP2pConfig {
fn from(config: P2pConfig) -> Self {
Self {
secret: Some(keypair_to_base64(&config.secret)),
private_key: Some(keypair_to_base64(&config.private_key)),
listen_ips: Some(config.listen_ips),
port: Some(config.port)
}
@ -111,7 +114,7 @@ impl From<P2pConfig> for PartialP2pConfig {
impl Default for PartialP2pConfig {
fn default() -> Self {
Self {
secret: None,
private_key: None,
listen_ips: Some(Vec::from(DEFAULT_P2P_LISTEN_IPS)),
port: Some(DEFAULT_P2P_PORT),
}
@ -121,21 +124,21 @@ impl Default for PartialP2pConfig {
impl Emptiable for PartialP2pConfig {
fn empty() -> Self {
Self{
secret: None,
private_key: None,
listen_ips: None,
port: None
}
}
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 {
fn merge(&mut self, mut other: Self) {
if let Some(x) = other.secret.take() {
let _ = self.secret.insert(x);
if let Some(x) = other.private_key.take() {
let _ = self.private_key.insert(x);
};
if let Some(x) = other.listen_ips.take() {
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)]

View file

@ -63,4 +63,19 @@ impl Mergeable for PartialRpcConfig {
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);
};
}
}
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
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
where
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

View file

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

View file

@ -2,15 +2,4 @@
pub use caretta_macros::Mergeable;
pub trait Mergeable: Sized {
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 caretta_core::{
config::{Config, ConfigError, PartialConfig, PartialP2pConfig, PartialStorageConfig},
utils::mergeable::Mergeable
utils::{emptiable::Emptiable, mergeable::Mergeable}
};
use libp2p::identity::Keypair;
use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell;
#[derive(Args, Clone, Debug)]
pub struct ConfigArgs {
#[arg(short = 'c', long = "config")]
pub file_path: Option<PathBuf>,
#[arg(skip)]
pub file_content: Option<PartialConfig>,
pub file_content: OnceCell<PartialConfig>,
#[command(flatten)]
pub args: PartialConfig,
}
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(
dirs::config_local_dir()
.unwrap()
.expect("Config user directory should be set")
.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 {
self.file_content.get_or_insert(
PartialConfig::read_from(self.get_file_path_or_default(app_name)).await.unwrap()
).clone()
async fn get_or_read_file_content(&self, app_name: &'static str) -> PartialConfig {
self.file_content.get_or_init(|| async {
PartialConfig::read_from(self.get_file_path_or_default(app_name)).await.expect("Config file should be invalid!")
}).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 file_content = self.get_or_read_file_content(app_name).await;
let args = self.args;
default.merge(file_content);
default.merge(args);
default.try_into().unwrap()
default.merge(self.to_partial_config_without_default(app_name).await);
default
}
pub async fn to_partial_config_without_default(&self, app_name: &'static str) -> PartialConfig {
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 {
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 caretta_core::utils::runnable::Runnable;
use caretta_core::{config::PartialConfig, utils::runnable::Runnable};
use crate::cli::ConfigArgs;
#[derive(Debug, Args)]
@ -12,6 +12,12 @@ pub struct ConfigListCommandArgs{
impl Runnable for ConfigListCommandArgs {
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
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_tcp(
tcp::Config::default(),

View file

@ -11,7 +11,7 @@ pub struct ServerCommandArgs {
}
impl Runnable for ServerCommandArgs {
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 crate::cli::Cli;
@ -8,6 +10,5 @@ mod ipc;
#[tokio::main]
async fn main() {
let args = Cli::parse();
args.run(APP_NAME).await;
}