Compare commits

..

No commits in common. "2cb1f50f57fad997d7c27c0c4a6a90f3c79f4a75" and "18e6d9239b755b18471397e098ec9328257e0192" have entirely different histories.

31 changed files with 295 additions and 170 deletions

View file

@ -15,13 +15,12 @@ test = ["dep:tempfile", "macros"]
[dependencies]
base64 = "0.22.1"
caretta-macros = { path = "macros", optional = true }
chrono.workspace = true
chrono-tz = "0.10.3"
ciborium.workspace = true
clap = {workspace = true, optional = true}
dirs = "6.0.0"
futures = "0.3.31"
caretta-macros = { path = "macros", optional = true }
libp2p.workspace = true
libp2p-core = { version = "0.43.0", features = ["serde"] }
libp2p-identity = { version = "0.2.11", features = ["ed25519", "peerid", "rand", "serde"] }

View file

@ -16,39 +16,40 @@ pub use rpc::*;
#[cfg(feature="desktop")]
use clap::Args;
#[derive(Clone, Debug)]
pub struct Config {
pub p2p: P2pConfig,
pub storage: StorageConfig,
pub rpc: RpcConfig,
pub trait Config: TryFrom<Self::PartialConfig>{
type PartialConfig: PartialConfig<Config = Self>;
}
pub trait PartialConfig: Emptiable + From<Self::Config> + Mergeable {
type Config: Config<PartialConfig = Self>;
}
#[cfg_attr(feature="desktop", derive(Args))]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct PartialConfig {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BaseConfig {
#[cfg_attr(feature="desktop", command(flatten))]
pub p2p: PartialP2pConfig,
p2p: PartialP2pConfig,
#[cfg_attr(feature="desktop", command(flatten))]
pub storage: PartialStorageConfig,
storage: PartialStorageConfig,
#[cfg_attr(feature="desktop", command(flatten))]
pub rpc: PartialRpcConfig,
rpc: PartialRpcConfig,
}
impl PartialConfig {
pub fn new() -> Self {
impl BaseConfig {
fn new() -> Self {
Self {
p2p : PartialP2pConfig::empty().with_new_secret(),
storage: PartialStorageConfig::empty(),
rpc: PartialRpcConfig::empty(),
}
}
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
toml::from_str(s)
}
pub fn into_toml(&self) -> Result<String, toml::ser::Error> {
fn into_toml(&self) -> Result<String, toml::ser::Error> {
toml::to_string(self)
}
pub async fn read_or_create<T>(path: T) -> Result<Self, ConfigError>
async fn read_or_create<T>(path: T) -> Result<Self, ConfigError>
where
T: AsRef<Path>
{
@ -57,7 +58,7 @@ impl PartialConfig {
}
Self::read_from(&path).await
}
pub async fn read_from<T>(path:T) -> Result<Self, ConfigError>
async fn read_from<T>(path:T) -> Result<Self, ConfigError>
where
T: AsRef<Path>
{
@ -67,7 +68,7 @@ impl PartialConfig {
let config: Self = toml::from_str(&content)?;
Ok(config)
}
pub async fn write_to<T>(&self, path:T) -> Result<(), ConfigError>
async fn write_to<T>(&self, path:T) -> Result<(), ConfigError>
where
T: AsRef<Path>
{
@ -83,29 +84,53 @@ impl PartialConfig {
}
}
impl Emptiable for PartialConfig {
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use crate::{tests::test_toml_serialize_deserialize, utils::{emptiable::Emptiable, mergeable::Mergeable}};
use super::{p2p::{P2pConfig, PartialP2pConfig}, PartialConfig};
#[derive(Debug, Deserialize, Serialize, PartialEq)]
pub struct TestConfig {
p2p: Option<PartialP2pConfig>
}
impl Default for TestConfig {
fn default() -> Self {
Self {
p2p: Some(PartialP2pConfig::default()),
}
}
}
impl Emptiable for TestConfig {
fn empty() -> Self {
Self {
p2p: PartialP2pConfig::empty(),
storage: PartialStorageConfig::empty(),
rpc: PartialRpcConfig::empty()
p2p: None,
}
}
fn is_empty(&self) -> bool {
self.p2p.is_empty() && self.rpc.is_empty() && self.storage.is_empty()
self.p2p.is_none()
}
}
impl Mergeable for TestConfig {
fn merge(&mut self, other: Self) {
if let Some(p2p) = other.p2p {
self.p2p = Some(p2p);
}
}
}
#[cfg(test)]
mod tests {
use libp2p::identity;
use super::*;
use crate::{tests::test_toml_serialize_deserialize};
#[tokio::test]
async fn test_p2p_config_serialize_deserialize() {
test_toml_serialize_deserialize(PartialConfig::empty());
test_toml_serialize_deserialize(TestConfig::empty());
test_toml_serialize_deserialize(TestConfig::default());
assert_eq!(TestConfig::empty(), toml::from_str("").unwrap());
assert_eq!("", &toml::to_string(&TestConfig::empty()).unwrap());
}
}

View file

@ -31,8 +31,9 @@ fn base64_to_keypair(base64: &str) -> Result<Keypair, Error> {
Ok(Keypair::from_protobuf_encoding(&vec)?)
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Deserialize, Serialize,)]
pub struct P2pConfig {
#[serde(with = "keypair_parser")]
pub secret: Keypair,
pub listen_ips: Vec<IpAddr>,
pub port: u16,
@ -81,6 +82,26 @@ impl TryFrom<PartialP2pConfig> for P2pConfig {
}
}
mod keypair_parser {
use libp2p::identity::Keypair;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(keypair: &Keypair, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer
{
serializer.serialize_str(&super::keypair_to_base64(keypair))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Keypair, D::Error>
where D: Deserializer<'de>
{
match super::base64_to_keypair(&String::deserialize(deserializer)?) {
Ok(x) => Ok(x),
Err(crate::error::Error::Base64Decode(_)) => Err(serde::de::Error::custom("Decoding base64 error")),
Err(_) => unreachable!()
}
}
}
#[cfg_attr(feature="desktop",derive(Args))]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct PartialP2pConfig {
@ -151,7 +172,7 @@ impl Mergeable for PartialP2pConfig {
mod tests {
use libp2p::identity;
use super::*;
use crate::{tests::test_toml_serialize_deserialize};
use crate::{config::PartialConfig, tests::test_toml_serialize_deserialize};
#[tokio::test]

View file

@ -10,7 +10,6 @@ use crate::config::error::ConfigError;
#[cfg(unix)]
static DEFAULT_SOCKET_PATH: &str = "caretta.sock";
#[derive(Clone, Debug)]
pub struct RpcConfig {
pub socket_path: PathBuf,
}
@ -25,7 +24,7 @@ impl TryFrom<PartialRpcConfig> for RpcConfig {
}
#[cfg_attr(feature="desktop", derive(Args))]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PartialRpcConfig {
pub socket_path: Option<PathBuf>,
}

View file

@ -5,7 +5,7 @@ use clap::Args;
#[cfg(any(test, feature="test"))]
use tempfile::tempdir;
use crate::{config::{ConfigError, PartialConfig}, utils::{emptiable::Emptiable, get_binary_name, mergeable::Mergeable}};
use crate::{config::{ConfigError, PartialConfig}, utils::{emptiable::Emptiable, mergeable::Mergeable}};
use libp2p::mdns::Config;
use serde::{Deserialize, Serialize};
@ -15,7 +15,7 @@ static CACHE_DATABASE_NAME: &str = "cache.sqlite";
#[cfg(any(test, feature="test"))]
use crate::tests::{GlobalTestDefault, TestDefault};
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct StorageConfig {
pub data_directory: PathBuf,
pub cache_directory: PathBuf,
@ -50,7 +50,7 @@ impl TryFrom<PartialStorageConfig> for StorageConfig {
}
}
#[cfg_attr(feature="desktop", derive(Args))]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PartialStorageConfig {
#[cfg_attr(feature="desktop", arg(long))]
pub data_directory: Option<PathBuf>,
@ -58,23 +58,6 @@ pub struct PartialStorageConfig {
pub cache_directory: Option<PathBuf>,
}
impl Default for PartialStorageConfig {
fn default() -> Self {
#[cfg(any(target_os="linux", target_os="macos", target_os="windows"))]
{
let mut data_dir = dirs::data_local_dir().unwrap();
data_dir.push(get_binary_name().unwrap());
let mut cache_dir = dirs::cache_dir().unwrap();
cache_dir.push(get_binary_name().unwrap());
Self {
data_directory: Some(data_dir),
cache_directory: Some(cache_dir)
}
}
}
}
impl From<StorageConfig> for PartialStorageConfig {
fn from(config: StorageConfig) -> PartialStorageConfig {
Self {

View file

@ -1,46 +1,15 @@
use tempfile::TempDir;
use tokio::sync::OnceCell;
use crate::{config::{P2pConfig, StorageConfig}, error::Error, global::GlobalConstant};
use crate::{config::{Config, PartialP2pConfig, PartialRpcConfig, PartialStorageConfig, StorageConfig}, error::Error, global::GlobalConstant};
pub static STORAGE_CONFIG: GlobalConstant<StorageConfig> = GlobalConstant::const_new(stringify!(STORAGE_CONFIG));
pub static P2P_CONFIG: GlobalConstant<P2pConfig> = GlobalConstant::const_new(stringify!(P2P_CONFIG));
pub static CONFIG: GlobalConfig = GlobalConfig::const_new();
pub struct GlobalConfig {
inner: OnceCell<Config>
}
#[cfg(test)]
mod tests {
use crate::global::{config::P2P_CONFIG, STORAGE_CONFIG};
impl GlobalConfig {
pub const fn const_new() -> Self {
Self{
inner: OnceCell::const_new()
}
}
pub async fn get_or_init(&'static self, source: Config) -> &'static Config {
self.inner.get_or_init(|| async {
source
}).await
}
pub fn get(&'static self) -> Option<&'static Config> {
self.inner.get()
}
pub fn get_and_unwrap(&'static self) -> &'static Config {
self.get().expect(&format!("Config is uninitialized!"))
}
#[cfg(any(test, feature=test))]
pub async fn get_or_init_test(&'static self) -> &'static Config {
let temp_dir = TempDir::new().unwrap().keep();
let mut data_dir = temp_dir.clone();
data_dir.push("data");
let mut cache_dir = temp_dir;
cache_dir.push("cache");
self.get_or_init(Config {
p2p: PartialP2pConfig::default().with_new_secret().try_into().unwrap(),
storage: StorageConfig {
data_directory: data_dir,
cache_directory: cache_dir,
},
rpc: PartialRpcConfig::default().try_into().unwrap(),
}).await
#[test]
fn test_global_constant_names() {
assert_eq!(STORAGE_CONFIG.name, stringify!(STORAGE_CONFIG));
assert_eq!(P2P_CONFIG.name, stringify!(P2P_CONFIG));
}
}

View file

@ -44,12 +44,12 @@ pub use tests::*;
#[cfg(test)]
mod tests {
use super::*;
use crate::{cache::migration::CacheMigrator, data::migration::DataMigrator, global::CONFIG, tests::GlobalTestDefault};
use crate::{cache::migration::CacheMigrator, data::migration::DataMigrator, global::STORAGE_CONFIG, tests::GlobalTestDefault};
pub async fn get_or_init_test_data_database() -> &'static DatabaseConnection{
DATA_DATABASE_CONNECTION.get_or_init(CONFIG.get_or_init_test_default().await.storage.get_cache_database_path(), DataMigrator).await
DATA_DATABASE_CONNECTION.get_or_init(STORAGE_CONFIG.get_or_init_test_default().await.get_data_database_path(), DataMigrator).await
}
pub async fn get_or_init_test_cache_database() -> &'static DatabaseConnection{
CACHE_DATABASE_CONNECTION.get_or_init(CONFIG.get_or_init_test_default()await.storage.get_cache_database_path(), CacheMigrator).await
CACHE_DATABASE_CONNECTION.get_or_init(STORAGE_CONFIG.get_or_init_test_default().await.get_cache_database_path(), CacheMigrator).await
}
}

View file

@ -58,7 +58,7 @@ impl<T> GlobalConstant<T> {
self.inner.get()
}
pub fn get_and_unwrap(&'static self) -> &'static T {
self.get().expect(&format!("{} is uninitialized!", self.name))
self.get().expect(&format!("{} is uninitialized!", &stringify!(self)))
}
}

View file

@ -1,2 +1,3 @@
pub mod server;
pub mod service;

16
core/src/rpc/server.rs Normal file
View file

@ -0,0 +1,16 @@
use tonic::transport::Server;
use crate::{proto::cached_peer_service_server::CachedPeerServiceServer, rpc::service::cached_peer::CachedPeerService};
pub async fn start_server() ->Result<(), Error> {
let addr = "[::1]:50051".parse()?;
let cached_peer_server = CachedPeerService::default();
Server::builder()
.add_service(CachedPeerServiceServer::new(cached_peer_server))
.serve(addr)
.await?;
Ok(())
}

View file

@ -2,16 +2,16 @@ use crate::{cache::entity::{CachedAddressEntity, CachedPeerEntity, CachedPeerMod
use futures::future::join_all;
use tonic::{Request, Response, Status};
use crate::proto::{cached_peer_service_server::{CachedPeerServiceServer}, CachedPeerListRequest, CachedPeerListResponse, CachedPeerMessage};
use crate::proto::{cached_peer_service_server::{CachedPeerService, CachedPeerServiceServer}, CachedPeerListRequest, CachedPeerListResponse, CachedPeerMessage};
use sea_orm::prelude::*;
#[derive(Debug, Default)]
pub struct CachedPeerService {}
pub struct CachedPeerServer {}
#[tonic::async_trait]
impl crate::proto::cached_peer_service_server::CachedPeerService for CachedPeerService {
impl CachedPeerService for CachedPeerServer {
async fn list(&self, request: Request<CachedPeerListRequest>) -> Result<Response<CachedPeerListResponse>, Status> {
println!("Got a request: {:?}", request);

View file

@ -29,3 +29,20 @@ pub trait TestDefault {
pub trait GlobalTestDefault<T: 'static> {
async fn get_or_init_test_default(&'static self) -> &'static T;
}
pub fn test_cbor_serialize_deserialize<T>(src: T)
where T: DeserializeOwned + Serialize + PartialEq + std::fmt::Debug
{
let mut buf: Vec<u8> = Vec::new();
ciborium::into_writer(&src, &mut buf).unwrap();
let dst: T = ciborium::from_reader(buf.as_slice()).unwrap();
assert_eq!(src, dst);
}
pub fn test_toml_serialize_deserialize<T>(src: T)
where T: DeserializeOwned + Serialize + PartialEq + std::fmt::Debug
{
let buf = toml::to_string(&src).unwrap();
let dst: T = toml::from_str(&buf).unwrap();
assert_eq!(src, dst);
}

View file

@ -36,12 +36,3 @@ pub fn utc_to_timestamp(utc: &DateTime<Utc>) -> Timestamp {
pub fn timestamp_to_utc(t: &Timestamp) -> DateTime<Utc> {
Utc.timestamp_opt(t.seconds, u32::try_from(t.nanos).unwrap()).unwrap()
}
pub fn get_binary_name() -> Option<String> {
std::env::current_exe()
.ok()?
.file_name()?
.to_str()?
.to_owned()
.into()
}

View file

@ -1,7 +1,8 @@
use std::{net::IpAddr, path::PathBuf};
use clap::Args;
use caretta_core::config::{PartialConfig,PartialP2pConfig, PartialStorageConfig, ConfigError};
use caretta_core::config::{BaseConfig, ConfigError};
use crate::config::{PartialP2pConfig, PartialStorageConfig};
use serde::{Deserialize, Serialize};
@ -12,9 +13,9 @@ pub struct ConfigArgs {
#[arg(short = 'c', long = "config")]
pub file_path: Option<PathBuf>,
#[arg(skip)]
pub file_content: Option<PartialConfig>,
pub file_content: Option<BaseConfig>,
#[command(flatten)]
pub args: PartialConfig,
pub args: BaseConfig,
}
@ -22,9 +23,9 @@ impl ConfigArgs {
pub fn get_file_path_or_default(&self) -> PathBuf {
self.file_path.clone().unwrap_or((*DEFAULT_CONFIG_FILE_PATH).clone())
}
pub async fn get_or_read_file_content(&mut self) -> &mut PartialConfig {
pub async fn get_or_read_file_content(&mut self) -> &mut BaseConfig {
self.file_content.get_or_insert(
PartialConfig::read_from(self.get_file_path_or_default()).await.unwrap()
BaseConfig::read_from(self.get_file_path_or_default()).await.unwrap()
)
}
}

View file

@ -2,11 +2,11 @@ use std::path::PathBuf;
mod args;
mod device;
mod peer;
mod server;
pub use args::*;
pub use device::*;
pub use peer::*;
pub use server::*;
pub trait RunnableCommand {
async fn run(self);

18
desktop/src/cli/server.rs Normal file
View file

@ -0,0 +1,18 @@
use clap::Args;
use caretta_core::utils::runnable::Runnable;
use libp2p::{noise, ping, swarm::{NetworkBehaviour, SwarmEvent}, tcp, yamux, Swarm};
use crate::{error::Error, global::P2P_CONFIG};
use super::ConfigArgs;
#[derive(Args, Debug)]
pub struct ServerCommandArgs {
#[command(flatten)]
config: ConfigArgs,
}
impl Runnable for ServerCommandArgs {
async fn run(self) {
P2P_CONFIG.get_and_unwrap().clone().launch_swarm();
}
}

View file

@ -1,5 +1,5 @@
[package]
name = "caretta-example-core"
name = "caretta-examples-core"
edition.workspace = true
version.workspace = true
description.workspace = true
@ -7,4 +7,4 @@ license.workspace = true
repository.workspace = true
[dependencies]
caretta.path = "../.."
dioxus.workspace = true

BIN
examples/core/assets/favicon.ico (Stored with Git LFS) Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -0,0 +1,46 @@
/* App-wide styling */
body {
background-color: #0f1116;
color: #ffffff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
}
#hero {
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
#links {
width: 400px;
text-align: left;
font-size: x-large;
color: white;
display: flex;
flex-direction: column;
}
#links a {
color: white;
text-decoration: none;
margin-top: 20px;
margin: 10px 0px;
border: white 1px solid;
border-radius: 5px;
padding: 10px;
}
#links a:hover {
background-color: #1f1f1f;
cursor: pointer;
}
#header {
max-width: 1200px;
}

View file

@ -1,2 +1 @@
pub mod rpc;
pub mod ui;

View file

@ -1 +0,0 @@
pub mod server;

View file

@ -0,0 +1 @@
pub mod plain;

View file

@ -0,0 +1,33 @@
use dioxus::prelude::*;
const FAVICON: Asset = asset!("/assets/favicon.ico");
const MAIN_CSS: Asset = asset!("/assets/main.css");
const HEADER_SVG: Asset = asset!("/assets/header.svg");
#[component]
pub fn App() -> Element {
rsx! {
document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "stylesheet", href: MAIN_CSS }
Hero {}
}
}
#[component]
pub fn Hero() -> Element {
rsx! {
div {
id: "hero",
img { src: HEADER_SVG, id: "header" }
div { id: "links",
a { href: "https://dioxuslabs.com/learn/0.6/", "📚 Learn Dioxus" }
a { href: "https://dioxuslabs.com/awesome", "🚀 Awesome Dioxus" }
a { href: "https://github.com/dioxus-community/", "📡 Community Libraries" }
a { href: "https://github.com/DioxusLabs/sdk", "⚙️ Dioxus Development Kit" }
a { href: "https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus", "💫 VSCode Extension" }
a { href: "https://discord.gg/XgGxMSkvUM", "👋 Community Discord" }
}
}
}
}

View file

@ -1,5 +1,5 @@
[package]
name = "caretta-example-desktop"
name = "caretta-examples-desktop"
version = "0.1.0"
edition = "2021"
@ -7,8 +7,12 @@ edition = "2021"
[dependencies]
clap.workspace = true
caretta = { path = "../..", features = ["desktop"] }
caretta-example-core.path = "../core"
libp2p.workspace = true
tokio.workspace = true
dioxus.workspace = true
caretta-desktop.path = "../../desktop"
caretta-examples-core.path = "../core"
[features]
default = ["desktop"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]

View file

@ -1,8 +1,5 @@
mod server;
use clap::{Parser, Subcommand};
use caretta::cli::*;
pub use server::*;
use caretta_desktop::cli::*;
#[derive(Debug, Parser)]
pub struct Cli {

View file

@ -1,19 +0,0 @@
use clap::Args;
use caretta::{error::Error, utils::runnable::Runnable};
use libp2p::{noise, ping, swarm::{NetworkBehaviour, SwarmEvent}, tcp, yamux, Swarm};
use super::ConfigArgs;
#[derive(Args, Debug)]
pub struct ServerCommandArgs {
#[command(flatten)]
config: ConfigArgs,
}
impl Runnable for ServerCommandArgs {
async fn run(self) {
let swarm_handler = P2P_CONFIG.get_and_unwrap().clone().launch_swarm();
let server_handler = caretta_example_core::rpc::server::start_server();
let (swarm_result, server_result) = tokio::try_join!(swarm_handler, server_handler).unwrap();
}
}

View file

@ -1,11 +1,5 @@
use crate::cli::Cli;
mod cli;
mod ipc;
#[tokio::main]
async fn main() {
let args = Cli::parse();
fn main() {
dioxus::launch(caretta_examples_core::ui::plain::App);
}

View file

@ -1,9 +1,17 @@
[package]
name = "caretta-example-mobile"
name = "caretta-examples-mobile"
version = "0.1.0"
authors = ["fluo10 <fluo10.dev@fireturtle.net>"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus.workspace = true
caretta-example-core.path = "../core"
caretta-examples-core.path = "../core"
[features]
default = ["mobile"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]

View file

@ -1,5 +1,5 @@
pub use caretta_core::*;
#[cfg(feature = "desktop")]
#[cfg(desktop)]
pub use caretta_desktop::*;
#[cfg(feature = "mobile")]
#[cfg(mobile)]
pub use caretta_mobile::*;