✨♻️➖➕ bevy_vanth, vanth, vanth_derive, vanth_transport: Many changes in preparation for networking
- Created new crate `bevy_vanth` with basic plugin structure for Bevy integration. - Refactored `Id` generation in `vanth` to use `OsRng` and removed redundant `to_u128_pair`/`from_u128_pair` methods. - Moved networking functionality into new `net` module with `Node`, `Packet`, and `Message` types. - Updated `vanth_derive` to use `proc-macro-crate` for reliable crate path resolution. - Added `rand` dependency to replace custom ID generation logic. - Updated `Cargo.toml`/`Cargo.lock` with new dependencies: `bevy_app`, `nix`, `cfg_aliases`, `proc-macro-crate`. - Modified `README.md` with improved project description. - Added commented clippy check in `flake.nix`. - Added `smol`, `async-process`, and `async-trait` dependencies in root and `vanth` crate. - Integrated `vanth` crate into `bevy_vanth` and added serde dependency. - Reorganized test files into module structure for `cli` and `vanth` crates. - Created new modules `compress` and `ecc` in `vanth`. - Implemented `Node` with async `run` method and `Backend` trait for networking in `vanth`. - Renamed `Memory` backend to `InMemoryStore` in `vanth` store module.
This commit is contained in:
parent
5262a266c0
commit
5afe3b61fb
34 changed files with 1887 additions and 483 deletions
10
crates/bevy_vanth/Cargo.toml
Normal file
10
crates/bevy_vanth/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "bevy_vanth"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bevy_ecs.workspace = true
|
||||
bevy_app.workspace = true
|
||||
serde.workspace = true
|
||||
vanth = { path = "../vanth" }
|
56
crates/bevy_vanth/src/lib.rs
Normal file
56
crates/bevy_vanth/src/lib.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use bevy_ecs::{component::Component, system::Query, world::World as BevyWorld};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vanth::{ActiveTask, Module, Root, Task, Vanth, entity::TaskId};
|
||||
|
||||
// TODO: Should there be one plugin per root, or one global plugin with many root components?
|
||||
// #[derive(Component)]
|
||||
// pub struct VanthRoot {}
|
||||
|
||||
pub struct BevyVanthPlugin {
|
||||
root: Root,
|
||||
}
|
||||
|
||||
impl BevyVanthPlugin {
|
||||
pub fn from_root(root: Root) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
}
|
||||
|
||||
// impl bevy_app::Plugin for BevyVanthPlugin {
|
||||
// fn build(&self, app: &mut bevy_app::App) {
|
||||
// app.add_systems(bevy_app::Update, process_active_tasks);
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct BevyPluginModule<M: Module>(M);
|
||||
|
||||
impl<M> From<M> for BevyPluginModule<M>
|
||||
where
|
||||
M: Module + Send + Sync,
|
||||
{
|
||||
fn from(value: M) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> bevy_app::Plugin for BevyPluginModule<M>
|
||||
where
|
||||
M: Module + Send + Sync + 'static,
|
||||
{
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
// app.add_systems(bevy_app::FixedPreUpdate, run_vanth).finish();
|
||||
}
|
||||
}
|
||||
|
||||
fn process_active_tasks(tasks: Query<()>) {}
|
||||
|
||||
pub fn run_task<I, O, T: Task<Input = I, Output = O>>(world: &mut BevyWorld, task: T) -> TaskId {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn creation_bevy_plugin<M>(module: M) -> impl bevy_app::Plugin
|
||||
where
|
||||
M: Module + Send + Sync + 'static,
|
||||
{
|
||||
BevyPluginModule(module)
|
||||
}
|
42
crates/bevy_vanth/tests/test.rs
Normal file
42
crates/bevy_vanth/tests/test.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use bevy_app::App;
|
||||
use bevy_vanth::BevyPluginModule;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vanth::{Module, Vanth};
|
||||
|
||||
mod bevy_tests {
|
||||
use bevy_app::Plugin;
|
||||
use vanth::Root;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct TestData {
|
||||
number: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct TestTask {
|
||||
a: TestData,
|
||||
b: TestData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct TestModule {}
|
||||
|
||||
impl Module for TestModule {
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bevy() {
|
||||
let root = Root::local();
|
||||
let module = TestModule {};
|
||||
let module_bevy_plugin = bevy_vanth::creation_bevy_plugin(module);
|
||||
let bevy_vanth_plugin = bevy_vanth::BevyVanthPlugin::from_root(root);
|
||||
|
||||
let mut app = App::new();
|
||||
|
||||
app.add_plugins(module_bevy_plugin);
|
||||
// app.add_plugins(bevy_vanth_plugin);
|
||||
}
|
||||
}
|
0
crates/cli/tests/nu/mod.rs
Normal file
0
crates/cli/tests/nu/mod.rs
Normal file
|
@ -7,6 +7,8 @@ use serde_json::{Value, json};
|
|||
use tempfile::tempdir;
|
||||
use vanth::{ContentHash, Vanth, hash as vanth_hash};
|
||||
|
||||
pub mod nu;
|
||||
|
||||
fn run_vanth(args: &[&str], input: Option<&str>) -> (String, String, i32) {
|
||||
let mut cmd = Command::cargo_bin("vanth").unwrap();
|
||||
let output = cmd.args(args).write_stdin(input.unwrap_or("")).output().unwrap();
|
|
@ -16,6 +16,10 @@ blake3.workspace = true
|
|||
vanth_derive = { path = "../vanth_derive" }
|
||||
rusqlite.workspace = true
|
||||
tracing.workspace = true
|
||||
rand = { workspace = true }
|
||||
smol.workspace = true
|
||||
async-process.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
|
85
crates/vanth/README.md
Normal file
85
crates/vanth/README.md
Normal file
|
@ -0,0 +1,85 @@
|
|||
# Vanth
|
||||
|
||||
## Library usage
|
||||
|
||||
Any type that implements `serde::Serialize` can be hashed using `vanth::hash` to produce a `vanth::ContentHash`.
|
||||
|
||||
```rust
|
||||
let x = "hello";
|
||||
assert_eq!(vanth::hash(&x).hex(), "ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f");
|
||||
```
|
||||
|
||||
Derive or implement the `vanth::Vanth` trait for types you want to store in the database.
|
||||
|
||||
```rust
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vanth::Vanth;
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct Data {
|
||||
value: u32,
|
||||
}
|
||||
```
|
||||
|
||||
This generates a method `Vanth::ty()` which returns a `vanth::Ty`. This should represent the type's fully qualified name - its module path followed by the type itself and any generics it has. E.g. `Data::ty().to_string()` could return `"my::crate::module::Data"`.
|
||||
|
||||
The derive macro only works for basic types right now and is not implemented for `std` types. Moving or renaming types or modules will change the type name, necessitating a database migration. This is not supported yet.
|
||||
|
||||
This should be used with caution. There are good reasons why `std::any::TypeId` is opaque.
|
||||
|
||||
### Database storage
|
||||
|
||||
```rust
|
||||
use vanth::store::{Store, StoreParams};
|
||||
|
||||
// Or use `Store::in_memory`.
|
||||
let mut store = Store::sqlite_from_path(
|
||||
"path/to/my_database.sqlite",
|
||||
StoreParams { create_if_not_exists: true, ..Default::default() },
|
||||
);
|
||||
let hash = store.write(Data { value: 5 }).unwrap();
|
||||
let my_data: Data = store.get_from_hash(hash).unwrap();
|
||||
```
|
||||
|
||||
## CLI usage
|
||||
|
||||
You can run the Vanth CLI with Nix using `nix run git+https://git.mascully.com/mascully/vanth.git`.
|
||||
|
||||
Use `--` to pass arguments, e.g. `nix run git+https://git.mascully.com/mascully/vanth.git -- --help`.
|
||||
|
||||
### Syntax
|
||||
|
||||
Write a component to the database:
|
||||
|
||||
```bash
|
||||
$ vanth write --db /path/to/db.sqlite --ty my::type::Name --value '{ "field": "value" }'
|
||||
a1cf81d8afe4e72604ea5c13bbd9b6cce14bd98c3a2f036f7156c4464a88ec09
|
||||
```
|
||||
|
||||
Get a component from the database:
|
||||
|
||||
```bash
|
||||
$ vanth get --db /path/to/db.sqlite --ty my::type::Name a1cf81d8afe4e72604ea5c13bbd9b6cce14bd98c3a2f036f7156c4464a88ec09
|
||||
{"field":"value"}
|
||||
```
|
||||
|
||||
Get all components of a type from the database:
|
||||
|
||||
```bash
|
||||
$ vanth get-all --db /path/to/db.sqlite --ty my::type::Name
|
||||
{"field":"value1"}
|
||||
{"field":"value2"}
|
||||
{"field":"value3"}
|
||||
```
|
||||
|
||||
Delete a component by its content hash:
|
||||
|
||||
```bash
|
||||
$ vanth delete --db /path/to/db.sqlite ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f
|
||||
```
|
||||
|
||||
Delete all components of a type:
|
||||
|
||||
```bash
|
||||
$ vanth delete-all --db /path/to/db.sqlite --ty my::type::Name
|
||||
```
|
|
@ -1,12 +1,12 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use vanth::{hash, Vanth};
|
||||
// use serde::{Deserialize, Serialize};
|
||||
// use vanth::{hash, Vanth};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
struct Foo {
|
||||
value: i32,
|
||||
}
|
||||
// #[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
// struct Foo {
|
||||
// value: i32,
|
||||
// }
|
||||
|
||||
fn main() {
|
||||
let x = "hello";
|
||||
println!("Hash: {:?}", hash(&x).hex());
|
||||
// let x = "hello";
|
||||
// println!("Hash: {:?}", hash(&x).hex());
|
||||
}
|
||||
|
|
11
crates/vanth/src/compress.rs
Normal file
11
crates/vanth/src/compress.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
pub trait Compress {
|
||||
type Output;
|
||||
|
||||
fn compress(&self) -> Self::Output;
|
||||
}
|
||||
|
||||
pub trait Decompress {
|
||||
type Output;
|
||||
|
||||
fn decompress(&self) -> Self::Output;
|
||||
}
|
1
crates/vanth/src/ecc.rs
Normal file
1
crates/vanth/src/ecc.rs
Normal file
|
@ -0,0 +1 @@
|
|||
//! Error-correcting codes or something.
|
|
@ -1,11 +1,13 @@
|
|||
use crate::hashing_serializer::{self, HashingSerializer};
|
||||
use crate::{hash, ComponentMarker, ContentHash, ModuleMarker, Task, TaskMarker, Ty, Vanth};
|
||||
use rand::TryRngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Debug, Display};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::hash;
|
||||
use crate::hashing_serializer::{self, HashingSerializer};
|
||||
|
||||
#[derive(Clone, Copy, Deserialize)]
|
||||
pub struct EntityId([u8; 32]);
|
||||
|
||||
|
@ -15,8 +17,14 @@ impl From<String> for EntityId {
|
|||
}
|
||||
}
|
||||
|
||||
pub type TaskId = Id<TaskMarker>;
|
||||
|
||||
pub type ModuleId = Id<ModuleMarker>;
|
||||
|
||||
pub type ComponentId = Id<ComponentMarker>;
|
||||
|
||||
/// A generic identifier type that can be used for different entity types
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Id<T: ?Sized> {
|
||||
/// The raw identifier value
|
||||
pub value: [u8; 32],
|
||||
|
@ -25,6 +33,20 @@ pub struct Id<T: ?Sized> {
|
|||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Copy for Id<T> {}
|
||||
|
||||
impl<T: ?Sized> Clone for Id<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self::new(self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> std::fmt::Debug for Id<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Id<{}>", std::any::type_name::<T>())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Id<T> {
|
||||
/// Create a new ID with the given value
|
||||
pub fn new(value: [u8; 32]) -> Self {
|
||||
|
@ -34,17 +56,15 @@ impl<T: ?Sized> Id<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create an ID from a u64 (fills only first 8 bytes)
|
||||
pub fn zero() -> Self {
|
||||
Self::new([0; 32])
|
||||
}
|
||||
|
||||
/// Generate a random ID
|
||||
pub fn random() -> Self {
|
||||
let mut value = [0u8; 32];
|
||||
// Simple random generation for demonstration
|
||||
for i in 0..32 {
|
||||
value[i] = (std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos() as u8)
|
||||
.wrapping_add(i as u8);
|
||||
}
|
||||
OsRng.try_fill_bytes(&mut value).unwrap();
|
||||
Self::new(value)
|
||||
}
|
||||
|
||||
|
@ -67,42 +87,6 @@ impl<T: ?Sized> Id<T> {
|
|||
}
|
||||
Self::new(bytes)
|
||||
}
|
||||
|
||||
/// Convert the ID to a pair of u128 values for handling larger values
|
||||
/// Returns (high_bits, low_bits)
|
||||
pub fn to_u128_pair(&self) -> (u128, u128) {
|
||||
let mut high = 0u128;
|
||||
let mut low = 0u128;
|
||||
|
||||
// First 16 bytes go to low
|
||||
for i in 0..16 {
|
||||
low |= (self.value[i] as u128) << (i * 8);
|
||||
}
|
||||
|
||||
// Next 16 bytes go to high
|
||||
for i in 0..16 {
|
||||
high |= (self.value[i + 16] as u128) << (i * 8);
|
||||
}
|
||||
|
||||
(high, low)
|
||||
}
|
||||
|
||||
/// Create an ID from a pair of u128 values
|
||||
pub fn from_u128_pair(high: u128, low: u128) -> Self {
|
||||
let mut bytes = [0u8; 32];
|
||||
|
||||
// Low bits fill first 16 bytes
|
||||
for i in 0..16 {
|
||||
bytes[i] = ((low >> (i * 8)) & 0xFF) as u8;
|
||||
}
|
||||
|
||||
// High bits fill next 16 bytes
|
||||
for i in 0..16 {
|
||||
bytes[i + 16] = ((high >> (i * 8)) & 0xFF) as u8;
|
||||
}
|
||||
|
||||
Self::new(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> PartialEq for Id<T> {
|
||||
|
@ -119,34 +103,28 @@ impl<T: ?Sized> Hash for Id<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Debug for Id<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let (high, low) = self.to_u128_pair();
|
||||
write!(f, "Id<{}>({:016x}{:016x})", std::any::type_name::<T>(), high, low)
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct Entity {
|
||||
pub id: Id<Entity>,
|
||||
pub components: HashMap<Ty, Box<dyn EntityComponent>>,
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Display for Id<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let (high, low) = self.to_u128_pair();
|
||||
write!(f, "{:016x}{:016x}", high, low)
|
||||
}
|
||||
pub trait EntityComponent: Debug {}
|
||||
|
||||
impl<T> EntityComponent for T where T: Component + Debug {}
|
||||
|
||||
pub trait Component: Clone + Vanth {
|
||||
fn component_id(&self) -> &'static str;
|
||||
}
|
||||
|
||||
pub struct ContentHash {
|
||||
value: [u8; 32],
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub struct EntityContents {
|
||||
components: Vec<ComponentContents>,
|
||||
}
|
||||
|
||||
impl ContentHash {}
|
||||
|
||||
pub trait Entity {
|
||||
fn entity_id() -> Id<dyn Entity>
|
||||
where
|
||||
Self: Sized;
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub struct ComponentContents {
|
||||
pub content_hash: ContentHash,
|
||||
pub ty: Ty,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub trait Component: Send + Sync + 'static {
|
||||
fn component_id() -> &'static str;
|
||||
}
|
||||
|
||||
// impl<'de, T> Component for T where T: Serialize + Deserialize<'de> {}
|
||||
|
|
|
@ -1,57 +1,44 @@
|
|||
use std::marker::PhantomData;
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use bevy_ecs::{prelude::*, query::QueryData};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bevy_ecs::{
|
||||
prelude::{Component as BevyComponent, Entity as BevyEntity},
|
||||
query::QueryData,
|
||||
};
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
|
||||
use crate::entity::EntityId;
|
||||
use crate::{
|
||||
entity::{Entity, EntityId, Id, TaskId},
|
||||
store::Store,
|
||||
};
|
||||
|
||||
pub mod compress;
|
||||
pub mod ecc;
|
||||
pub mod entity;
|
||||
pub mod hashing_serializer;
|
||||
pub mod network;
|
||||
pub mod nix;
|
||||
pub mod std_impls;
|
||||
pub mod store;
|
||||
|
||||
pub use hashing_serializer::hash;
|
||||
pub use network::Node;
|
||||
pub use vanth_derive::Vanth;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum Error {
|
||||
Other(String),
|
||||
}
|
||||
|
||||
|
||||
/// A view of all of the [`Node`]s in a cluster.
|
||||
pub struct Network {
|
||||
// TODO
|
||||
}
|
||||
|
||||
/// A Vanth server.
|
||||
pub struct Node {
|
||||
// TODO
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
impl From<smol::io::Error> for Error {
|
||||
fn from(value: smol::io::Error) -> Self {
|
||||
Self::Other(value.to_string())
|
||||
}
|
||||
|
||||
pub fn entity_count(&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn save(entity_id: impl Into<EntityId>) -> Result<()> {
|
||||
// TODO
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub fn load(entity_id: impl Into<EntityId>) -> Result<Option<EntityContents>> {
|
||||
// // TODO
|
||||
// Ok(None)
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
@ -75,65 +62,57 @@ pub struct Value {
|
|||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
pub struct EventTy(Ty);
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
pub struct TaskTy(Ty);
|
||||
|
||||
/// A wrapper for the fully-qualified name of a Rust type. This should be univerisally unique for a given type within a
|
||||
/// given project.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, Hash)]
|
||||
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
|
||||
pub struct Ty {
|
||||
pub path: Vec<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Ty {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Ty {
|
||||
fn to_string(&self) -> String {
|
||||
self.path.join("::")
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Ty {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> PartialEq<T> for Ty {
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
self.to_string() == *other.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, BevyComponent, Serialize, PartialEq, Eq, Hash, Vanth)]
|
||||
pub struct VanthVersion {
|
||||
pub version_number: u64,
|
||||
pub extra_data: u64,
|
||||
}
|
||||
|
||||
/// All types stored in the Vanth database should implement this trait.
|
||||
pub trait Vanth {
|
||||
/// Get the [`Ty`] representing this type.
|
||||
fn ty() -> Ty;
|
||||
}
|
||||
|
||||
macro_rules! impl_vanth {
|
||||
// TODO
|
||||
() => {};
|
||||
}
|
||||
|
||||
// impl_vanth!(std::string::String)
|
||||
|
||||
// TODO: Impl for different tuple sizes
|
||||
pub trait VanthTuple {}
|
||||
|
||||
// #[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
// pub struct EntityContents {
|
||||
// components: Vec<ComponentContents>
|
||||
// }
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ComponentContents<T: Vanth> {
|
||||
content_hash: ContentHash,
|
||||
data: Vec<u8>,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
// use a macro to implement VanthTuiple here.
|
||||
|
||||
/// A 32 byte BLAKE3 hash representing the contents of some value.
|
||||
///
|
||||
/// This can be generated with the [`hash`] function.
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Component, Serialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Copy, Clone, Debug, Deserialize, BevyComponent, Serialize, PartialEq, Eq, Hash, Vanth)]
|
||||
pub struct ContentHash {
|
||||
pub hash: [u8; 32],
|
||||
}
|
||||
|
@ -144,13 +123,13 @@ impl ContentHash {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Component, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, BevyComponent, Serialize)]
|
||||
pub struct Reference<T: Clone + Serialize> {
|
||||
value: ReferenceValue,
|
||||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Component, Serialize)]
|
||||
#[derive(Clone, Debug, Deserialize, BevyComponent, Serialize)]
|
||||
pub enum ReferenceValue {
|
||||
Absent,
|
||||
Retrieving(ReferenceRetrievalTask),
|
||||
|
@ -167,7 +146,7 @@ impl<T: Clone + Serialize> Reference<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Deserialize, Serialize)]
|
||||
#[derive(BevyComponent, Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ReferenceRetrievalTask {}
|
||||
|
||||
impl Future for ReferenceRetrievalTask {
|
||||
|
@ -182,5 +161,157 @@ pub struct Handle<T> {
|
|||
_marker: PhantomData<T>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Task: Vanth + Serialize + DeserializeOwned {
|
||||
type Input: Vanth + Serialize;
|
||||
type Output: Vanth + Serialize + DeserializeOwned;
|
||||
type Resource: Vanth;
|
||||
}
|
||||
|
||||
pub struct TaskMarker;
|
||||
pub struct ModuleMarker;
|
||||
pub struct ComponentMarker;
|
||||
|
||||
#[derive(BevyComponent)]
|
||||
pub enum ActiveTask<T: Task> {
|
||||
Requested(T::Input),
|
||||
// Running(Box<dyn Future<Output = T::Output> + Send + Sync>),
|
||||
// TODO
|
||||
Running(TaskId),
|
||||
Completed(T::Output),
|
||||
}
|
||||
|
||||
pub struct TaskCache {
|
||||
pub values: HashMap<TaskId, Vec<u8>>,
|
||||
}
|
||||
|
||||
/// A representation of the approximate computational cost of completing a task.
|
||||
pub struct Cost {
|
||||
value: f64,
|
||||
}
|
||||
|
||||
/// A world which Vanth entities live in. Lifetimes `'v` of [`Vanth<'v>`] types are tied to the lifetime of the `Root`.
|
||||
pub struct Root {}
|
||||
#[derive(Debug)]
|
||||
pub struct Root {
|
||||
authority: Authority,
|
||||
current_epoch: u64,
|
||||
store: Store,
|
||||
registered_components: HashSet<Ty>,
|
||||
registered_modules: HashMap<Ty, BoxedModule>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BoxedModule {}
|
||||
|
||||
impl Root {
|
||||
pub fn local() -> Self {
|
||||
Self::new(Authority::Local, Store::in_memory().unwrap())
|
||||
}
|
||||
|
||||
pub fn new(authority: Authority, store: Store) -> Self {
|
||||
Self {
|
||||
authority,
|
||||
current_epoch: 0,
|
||||
store,
|
||||
registered_components: HashSet::new(),
|
||||
registered_modules: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_module<M: Module>(&mut self) {
|
||||
let boxed_module = BoxedModule {};
|
||||
self.registered_modules.insert(M::ty(), boxed_module);
|
||||
}
|
||||
|
||||
pub async fn run_module_task<R, M, T>(&mut self, module: &mut M, input: T::Input) -> T::Output
|
||||
where
|
||||
M: Module + ProvideResource<R> + ExecuteTask<T>,
|
||||
T: Task<Resource = R>,
|
||||
{
|
||||
let mut resources = module.get_resource();
|
||||
module.execute_task(input, &mut resources).await
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Module: Vanth + Serialize + DeserializeOwned {}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ExecuteTask<T>
|
||||
where
|
||||
T: Task,
|
||||
{
|
||||
async fn execute_task(&self, input: T::Input, resource: &mut T::Resource) -> T::Output;
|
||||
}
|
||||
|
||||
pub trait ProvideResource<R>: Module {
|
||||
fn get_resource(&self) -> R;
|
||||
}
|
||||
|
||||
// TODO: Implement for other tuple sizes.
|
||||
impl<M, R1, R2> ProvideResource<(R1, R2)> for M
|
||||
where
|
||||
M: Module + ProvideResource<R1> + ProvideResource<R2>,
|
||||
{
|
||||
fn get_resource(&self) -> (R1, R2) {
|
||||
(self.get_resource(), self.get_resource())
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, R1, R2, R3> ProvideResource<(R1, R2, R3)> for M
|
||||
where
|
||||
M: Module + ProvideResource<R1> + ProvideResource<R2> + ProvideResource<R3>,
|
||||
{
|
||||
fn get_resource(&self) -> (R1, R2, R3) {
|
||||
(self.get_resource(), self.get_resource(), self.get_resource())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Shell: Module {
|
||||
// <Self as Module>::Monads
|
||||
}
|
||||
|
||||
pub struct ModuleRegistration {
|
||||
// types: Vec<Ty>,
|
||||
// events: Vec<EventTy>,
|
||||
handler: Box<dyn ModuleRegistrationHandler>,
|
||||
}
|
||||
|
||||
impl ModuleRegistration {
|
||||
fn add_type<T: Vanth>(&mut self) {
|
||||
self.handler.add_type(T::ty());
|
||||
}
|
||||
|
||||
fn add_task<T: Task>(&mut self) -> TaskId {
|
||||
todo!()
|
||||
// self.handler.add_task(T::Input::ty(), T::Output::ty())
|
||||
}
|
||||
|
||||
fn add_resource<R: Vanth>(&mut self) {
|
||||
self.handler.add_resource(R::ty())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ModuleRegistrationHandler {
|
||||
fn add_type(&self, ty: Ty);
|
||||
fn add_task(&self, input_ty: Ty, output_ty: Ty) -> TaskId;
|
||||
fn add_resource(&self, resource_ty: Ty);
|
||||
}
|
||||
|
||||
pub trait TaskSystem {}
|
||||
|
||||
// impl <F> TaskSystem for F where F: Fn()
|
||||
|
||||
impl ModuleRegistration {}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub enum Authority {
|
||||
/// No networking or synchronization.
|
||||
Local,
|
||||
Master(MasterAuthority),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub struct MasterAuthority {
|
||||
// TODO
|
||||
public_key: Vec<u8>,
|
||||
}
|
||||
|
|
145
crates/vanth/src/network.rs
Normal file
145
crates/vanth/src/network.rs
Normal file
|
@ -0,0 +1,145 @@
|
|||
use std::collections::HashMap;
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::net::UdpSocket;
|
||||
|
||||
use crate::entity::{Entity, EntityContents};
|
||||
use crate::{ContentHash, Error};
|
||||
use crate::{
|
||||
Vanth,
|
||||
entity::{EntityId, Id},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NetworkError {
|
||||
Timeout {
|
||||
duration_waited_secs: f64,
|
||||
},
|
||||
Other(String)
|
||||
}
|
||||
|
||||
pub struct Packet {
|
||||
pub source_node: Id<Node>,
|
||||
pub signature: Vec<u8>,
|
||||
pub message: Message,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub struct Message {
|
||||
pub events: Vec<Event>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub enum Event {
|
||||
RequestValue(ContentHash),
|
||||
UpsertEntity(EntityContents),
|
||||
DeleteEntity(EntityContents),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub enum Address {
|
||||
Uri(String),
|
||||
Id(Id<Address>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub struct NetworkParams {
|
||||
id: Id<Network>,
|
||||
initial_peers: Vec<Address>,
|
||||
}
|
||||
|
||||
/// A view of all of the [`Node`]s in a cluster.
|
||||
#[derive(Debug)]
|
||||
pub struct Network {
|
||||
// TODO
|
||||
id: Id<Network>,
|
||||
peers: HashMap<Id<Node>, Peer>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub struct Peer {
|
||||
id: Id<Node>,
|
||||
last_confirmed_epoch: u64,
|
||||
}
|
||||
|
||||
/// A Vanth server.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub struct Node {
|
||||
id: Id<Node>,
|
||||
config: NodeConfig,
|
||||
ip_address: Option<Ipv6Addr>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Vanth)]
|
||||
pub struct NodeConfig {
|
||||
pub port: u16,
|
||||
networks: Vec<NetworkParams>,
|
||||
}
|
||||
|
||||
impl Default for NodeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// Sidereal orbital period of Vanth in 100's of seconds.
|
||||
port: 8241,
|
||||
networks: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn from_config(config: impl Into<NodeConfig>) -> Self {
|
||||
Self {
|
||||
id: Id::random(),
|
||||
config: config.into(),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_id(id: Id<Node>, config: impl Into<NodeConfig>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
config: config.into(),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entity_count(&self) -> usize {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn run(self) -> Result<(), Error> {
|
||||
let socket = UdpSocket::bind(("127.0.0.1", self.config.port)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(entity_id: impl Into<EntityId>) -> Result<(), Error> {
|
||||
// TODO
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to open a UDP connection to the provided [`Node`].
|
||||
pub async fn connect_to(network: Id<Network>, node: &Node) -> Result<(), Error> {
|
||||
// TODO
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// pub fn load(entity_id: impl Into<EntityId>) -> Result<Option<EntityContents>> {
|
||||
// // TODO
|
||||
// Ok(None)
|
||||
// }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Backend {
|
||||
type ConnectionHandle;
|
||||
type Error;
|
||||
|
||||
// TODO: Improve efficiency or something.
|
||||
async fn connect<'a>(&mut self, address: &Address) -> Result<Self::ConnectionHandle, Self::Error>;
|
||||
async fn close(&mut self, connection: Self::ConnectionHandle) -> Result<(), Self::Error>;
|
||||
async fn poll(&mut self) -> impl Iterator<Item = Result<(Self::ConnectionHandle, Vec<u8>), Self::Error>>;
|
||||
async fn send(&mut self, data: &[u8]) -> Result<(), Self::Error>;
|
||||
}
|
68
crates/vanth/src/std_impls.rs
Normal file
68
crates/vanth/src/std_impls.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use super::{Ty, Vanth};
|
||||
|
||||
impl Vanth for () {
|
||||
fn ty() -> Ty {
|
||||
Ty {
|
||||
path: vec!["()".into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_tuple_vanth {
|
||||
($($T:ident),+ $(,)?) => {
|
||||
impl<$($T),+> Vanth for ($($T,)+)
|
||||
where
|
||||
$($T: Vanth),+
|
||||
{
|
||||
fn ty() -> Ty {
|
||||
// Join the inner type strings with ", " and wrap in parentheses.
|
||||
Ty {
|
||||
path: vec![format!(
|
||||
"({})",
|
||||
vec![$($T::ty().to_string()),+].join(", ")
|
||||
)],
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_tuple_vanth!(T1);
|
||||
impl_tuple_vanth!(T1, T2);
|
||||
impl_tuple_vanth!(T1, T2, T3);
|
||||
impl_tuple_vanth!(T1, T2, T3, T4);
|
||||
impl_tuple_vanth!(T1, T2, T3, T4, T5);
|
||||
impl_tuple_vanth!(T1, T2, T3, T4, T5, T6);
|
||||
impl_tuple_vanth!(T1, T2, T3, T4, T5, T6, T7);
|
||||
impl_tuple_vanth!(T1, T2, T3, T4, T5, T6, T7, T8);
|
||||
impl_tuple_vanth!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
|
||||
impl_tuple_vanth!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
|
||||
impl_tuple_vanth!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
|
||||
impl_tuple_vanth!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
|
||||
|
||||
impl<T> Vanth for Option<T>
|
||||
where
|
||||
T: Vanth,
|
||||
{
|
||||
fn ty() -> Ty {
|
||||
Ty {
|
||||
path: vec![format!("Option<{}>", T::ty().to_string())],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Vanth for Result<T, E>
|
||||
where
|
||||
T: Vanth,
|
||||
E: Vanth,
|
||||
{
|
||||
fn ty() -> Ty {
|
||||
Ty {
|
||||
path: vec![format!(
|
||||
"Result<{}, {}>",
|
||||
T::ty().to_string(),
|
||||
E::ty().to_string()
|
||||
)],
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
use std::{collections::HashMap, marker::PhantomData, path::PathBuf};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
marker::PhantomData,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use rusqlite::{Connection, named_params, params};
|
||||
|
||||
|
@ -6,7 +10,7 @@ use bevy_ecs::prelude::*;
|
|||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use tracing::trace;
|
||||
|
||||
use crate::{ComponentContents, ContentHash, Ty, Vanth, hash};
|
||||
use crate::{ContentHash, Ty, Vanth, entity::ComponentContents, hash};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Store {
|
||||
|
@ -58,7 +62,7 @@ impl Default for StoreParams {
|
|||
|
||||
impl Store {
|
||||
/// Use an SQLite backend with a database file at the provided path.
|
||||
pub fn sqlite_from_path(path: PathBuf, params: StoreParams) -> Result<Self> {
|
||||
pub fn sqlite_from_path(path: impl AsRef<Path>, params: StoreParams) -> Result<Self> {
|
||||
use rusqlite::OpenFlags;
|
||||
// Base flags for URI handling and disabling mutexes.
|
||||
let mut flags = OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX;
|
||||
|
@ -76,7 +80,7 @@ impl Store {
|
|||
}
|
||||
|
||||
// Open the SQLite connection with the computed flags.
|
||||
let connection = rusqlite::Connection::open_with_flags(path, flags)?;
|
||||
let connection = rusqlite::Connection::open_with_flags(path.as_ref(), flags)?;
|
||||
Ok(Self {
|
||||
backend: Box::new(Sqlite { connection }),
|
||||
})
|
||||
|
@ -85,7 +89,7 @@ impl Store {
|
|||
/// Use an in-memory backend.
|
||||
pub fn in_memory() -> Result<Self> {
|
||||
Ok(Self {
|
||||
backend: Box::new(Memory::new()),
|
||||
backend: Box::new(InMemoryStore::new()),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -102,14 +106,14 @@ impl Store {
|
|||
self.backend.get_from_hash(ty, content_hash)
|
||||
}
|
||||
|
||||
pub fn get_all_of_type<T: Vanth>(&mut self) -> Result<Vec<ComponentContents<T>>> {
|
||||
pub fn get_all_of_type<T: Vanth>(&mut self) -> Result<Vec<ComponentContents>> {
|
||||
let raw_items = self.backend.get_all_of_ty(T::ty())?;
|
||||
let mut results = Vec::new();
|
||||
for (content_hash, data) in raw_items {
|
||||
results.push(ComponentContents {
|
||||
_marker: PhantomData,
|
||||
content_hash,
|
||||
data,
|
||||
ty: T::ty(),
|
||||
});
|
||||
}
|
||||
Ok(results)
|
||||
|
@ -153,6 +157,7 @@ pub struct Cache {
|
|||
// backend: Backend,
|
||||
}
|
||||
|
||||
// TODO: Change to storing entities (groups of components) instead of components.
|
||||
pub trait Backend: std::fmt::Debug + Send {
|
||||
fn get_from_hash(&mut self, ty: Ty, content_hash: ContentHash) -> Result<Option<Vec<u8>>>;
|
||||
|
||||
|
@ -229,7 +234,7 @@ impl Backend for Sqlite {
|
|||
|
||||
drop(statement);
|
||||
transaction.commit()?;
|
||||
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
|
@ -272,17 +277,17 @@ impl Backend for Sqlite {
|
|||
|
||||
/// In-memory storage with one table per type.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Memory {
|
||||
pub struct InMemoryStore {
|
||||
tables: HashMap<Ty, HashMap<ContentHash, Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl Memory {
|
||||
impl InMemoryStore {
|
||||
pub fn new() -> Self {
|
||||
Self { tables: HashMap::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for Memory {
|
||||
impl Backend for InMemoryStore {
|
||||
fn get_from_hash(&mut self, ty: Ty, content_hash: ContentHash) -> Result<Option<Vec<u8>>> {
|
||||
Ok(self.tables.get(&ty).and_then(|table| table.get(&content_hash)).cloned())
|
||||
}
|
||||
|
@ -315,3 +320,13 @@ impl Backend for Memory {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// One table per type. Keys and values are both blobs.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct FsStore {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
// impl Backend for FsStore {
|
||||
|
||||
// }
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/// A 192-bit hash value.
|
||||
pub struct Hash([u8; 24]);
|
||||
|
||||
impl Hash {
|
||||
pub fn from_bytes(value: [u8; 24]) -> Self {
|
||||
Hash(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Hash {
|
||||
/// The Base52 representation of the hash. This will always be 34 ASCII characters long.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut result = String::with_capacity(34);
|
||||
for chunk in self.0.chunks(4) {
|
||||
let mut value = 0u32;
|
||||
for (i, &byte) in chunk.iter().enumerate() {
|
||||
value |= (byte as u32) << (8 * i);
|
||||
}
|
||||
|
||||
for _ in 0..6 {
|
||||
let digit = value % 52;
|
||||
value /= 52;
|
||||
let c = if digit < 26 {
|
||||
(b'A' + digit as u8) as char
|
||||
} else {
|
||||
(b'a' + (digit - 26) as u8) as char
|
||||
};
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
write!(f, "{}", result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash {
|
||||
pub fn to_string_truncated(&self, length: usize) -> String {
|
||||
self.to_string()[0..length].to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hash_to_string() {
|
||||
let pairs = [(0u8, "AAAAAA"), (255u8, "ZZZZZZ"), (1u8, "AAAAAB")];
|
||||
let input = [0u8; 24];
|
||||
let hash = Hash::from_bytes(input);
|
||||
let result = hash.to_string();
|
||||
|
||||
assert_eq!(result.len(), 34);
|
||||
|
||||
// String should only contain A-Z and a-z
|
||||
assert!(result.chars().all(|c| c.is_ascii_alphabetic()));
|
||||
|
||||
// With all zero bytes, should start with 'AAAAAA'
|
||||
assert_eq!(&result[0..6], "AAAAAA");
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
use bevy_ecs::component::Component;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use vanth::Vanth;
|
||||
|
||||
// TODO: derive `Vanth`
|
||||
#[derive(Debug, Deserialize, Component, Serialize)]
|
||||
struct Foo {}
|
||||
|
||||
#[test]
|
||||
fn test_derive() {
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct Foo<T: Vanth> {
|
||||
field_a: i32,
|
||||
field_b: String,
|
||||
inner: T,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct Bar {
|
||||
field_a: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct Qux<T: Vanth, S: Vanth> {
|
||||
field_a: i32,
|
||||
field_b: String,
|
||||
inner: T,
|
||||
inner_2: S,
|
||||
}
|
||||
|
||||
let base = "integration::derive::";
|
||||
|
||||
assert_eq!(Bar::ty(), format!("{base}Bar"));
|
||||
assert_eq!(Foo::<Bar>::ty(), format!("{base}Foo<{base}Bar>"));
|
||||
assert_eq!(
|
||||
Qux::<Bar, Foo<Bar>>::ty(),
|
||||
format!("{base}Qux<{base}Bar,{base}Foo<{base}Bar>>")
|
||||
);
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use vanth::{Node, Reference};
|
||||
|
||||
mod derive;
|
||||
mod fs;
|
||||
mod reference;
|
||||
mod store;
|
47
crates/vanth/tests/net/mod.rs
Normal file
47
crates/vanth/tests/net/mod.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use smol::channel::{self, Receiver, Sender};
|
||||
use vanth::{
|
||||
entity::Id,
|
||||
network::{self, Address, Packet},
|
||||
};
|
||||
|
||||
struct TestNetBackend {
|
||||
sockets: HashMap<Id<Address>, (Sender<Packet>, Receiver<Packet>)>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl network::Backend for TestNetBackend {
|
||||
type ConnectionHandle = Id<Address>;
|
||||
type Error = String;
|
||||
|
||||
async fn connect<'a>(&mut self, address: &Address) -> Result<Self::ConnectionHandle, Self::Error> {
|
||||
let id = match address {
|
||||
Address::Id(id) => id,
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
let socket = channel::unbounded();
|
||||
assert!(self.sockets.insert(*id, socket).is_none());
|
||||
Ok(*id)
|
||||
}
|
||||
|
||||
async fn close(&mut self, connection: Self::ConnectionHandle) -> Result<(), Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn poll(&mut self) -> impl Iterator<Item = Result<(Self::ConnectionHandle, Vec<u8>), Self::Error>> {
|
||||
todo!();
|
||||
[].into_iter()
|
||||
}
|
||||
|
||||
async fn send(&mut self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network() {
|
||||
// let id_1 = Id::random();
|
||||
// let address_1 = Address::Id(id);
|
||||
}
|
138
crates/vanth/tests/test.rs
Normal file
138
crates/vanth/tests/test.rs
Normal file
|
@ -0,0 +1,138 @@
|
|||
use bevy_ecs::component::Component;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use vanth::{Node, Reference, Vanth};
|
||||
|
||||
mod fs;
|
||||
mod net;
|
||||
mod reference;
|
||||
mod store;
|
||||
|
||||
// TODO: derive `Vanth`
|
||||
#[derive(Debug, Deserialize, Component, Serialize)]
|
||||
struct Foo {}
|
||||
|
||||
#[test]
|
||||
fn test_derive() {
|
||||
mod vanth {}
|
||||
|
||||
use ::vanth::Vanth;
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct Foo<T: ::vanth::Vanth> {
|
||||
field_a: i32,
|
||||
field_b: String,
|
||||
inner: T,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct Bar {
|
||||
field_a: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct Qux<T: Vanth, S: Vanth> {
|
||||
field_a: i32,
|
||||
field_b: String,
|
||||
inner: T,
|
||||
inner_2: S,
|
||||
}
|
||||
|
||||
let base = "test::";
|
||||
|
||||
assert_eq!(Bar::ty(), format!("{base}Bar"));
|
||||
assert_eq!(Foo::<Bar>::ty(), format!("{base}Foo<{base}Bar>"));
|
||||
assert_eq!(
|
||||
Qux::<Bar, Foo<Bar>>::ty(),
|
||||
format!("{base}Qux<{base}Bar,{base}Foo<{base}Bar>>")
|
||||
);
|
||||
}
|
||||
|
||||
mod task_tests {
|
||||
use async_trait::async_trait;
|
||||
use smol::block_on;
|
||||
use vanth::{ExecuteTask, Module, Root, Task};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct TestData(u32);
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct TestTask {
|
||||
a: TestData,
|
||||
b: TestData,
|
||||
}
|
||||
|
||||
impl Task for TestTask {
|
||||
type Input = (TestData, TestData);
|
||||
type Output = TestData;
|
||||
type Resource = ();
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct TestModule {}
|
||||
|
||||
impl Module for TestModule {}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecuteTask<TestTask> for TestModule {
|
||||
async fn execute_task(&self, input: (TestData, TestData), resource: &mut ()) -> TestData {
|
||||
let (TestData(a), TestData(b)) = input;
|
||||
TestData(a + b)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task() {
|
||||
let root = Root::local();
|
||||
let module = TestModule {};
|
||||
let input = (TestData(2), TestData(3));
|
||||
let mut resources = ();
|
||||
|
||||
let result = block_on(module.execute_task(input, &mut resources));
|
||||
assert_eq!(result.0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
mod module_tests {
|
||||
use async_trait::async_trait;
|
||||
use smol::block_on;
|
||||
use vanth::{ExecuteTask, Module, Root, Task};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct TestData(u32);
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct TestTask {
|
||||
a: TestData,
|
||||
b: TestData,
|
||||
}
|
||||
|
||||
impl Task for TestTask {
|
||||
type Input = (TestData, TestData);
|
||||
type Output = TestData;
|
||||
type Resource = ();
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Vanth)]
|
||||
struct TestModule {}
|
||||
|
||||
impl Module for TestModule {}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecuteTask<TestTask> for TestModule {
|
||||
async fn execute_task(&self, input: (TestData, TestData), resource: &mut ()) -> TestData {
|
||||
let (TestData(a), TestData(b)) = input;
|
||||
TestData(a + b)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task() {
|
||||
let mut root = Root::local();
|
||||
let module = TestModule {};
|
||||
// let module_id = root.register_module(module);
|
||||
}
|
||||
}
|
|
@ -9,4 +9,5 @@ proc-macro = true
|
|||
[dependencies]
|
||||
quote.workspace = true
|
||||
syn.workspace = true
|
||||
proc-macro2.workspace = true
|
||||
proc-macro2.workspace = true
|
||||
proc-macro-crate.workspace = true
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use proc_macro::TokenStream;
|
||||
use proc_macro_crate::{crate_name, FoundCrate};
|
||||
use proc_macro2::Span;
|
||||
use quote::quote;
|
||||
use syn::parse_quote;
|
||||
use syn::{DeriveInput, GenericParam, Generics, parse_macro_input};
|
||||
|
@ -11,6 +13,15 @@ pub fn vanth_derive(input: TokenStream) -> TokenStream {
|
|||
|
||||
let mut generics = input.generics.clone();
|
||||
|
||||
let vanth_path = match crate_name("vanth") {
|
||||
Ok(FoundCrate::Itself) => quote!(crate),
|
||||
Ok(FoundCrate::Name(name)) => {
|
||||
let ident = syn::Ident::new(&name, Span::call_site());
|
||||
quote!(::#ident)
|
||||
}
|
||||
Err(_) => quote!(vanth),
|
||||
};
|
||||
|
||||
let type_params: Vec<syn::Ident> = generics
|
||||
.params
|
||||
.iter()
|
||||
|
@ -25,7 +36,7 @@ pub fn vanth_derive(input: TokenStream) -> TokenStream {
|
|||
|
||||
let mut where_clause = generics.where_clause.clone().unwrap_or_else(|| parse_quote!(where));
|
||||
for tp in &type_params {
|
||||
where_clause.predicates.push(parse_quote!(#tp : vanth::Vanth));
|
||||
where_clause.predicates.push(parse_quote!(#tp : #vanth_path::Vanth));
|
||||
}
|
||||
|
||||
let (impl_generics, ty_generics, _) = generics.split_for_impl();
|
||||
|
@ -34,19 +45,19 @@ pub fn vanth_derive(input: TokenStream) -> TokenStream {
|
|||
quote! { String::new() }
|
||||
} else {
|
||||
quote! {
|
||||
format!("<{}>", [#(#type_params::ty().path.join("::")),*].join(","))
|
||||
format!("<{}>", [#( <#type_params as #vanth_path::Vanth>::ty().path.join("::") ),*].join(","))
|
||||
}
|
||||
};
|
||||
|
||||
let expanded = quote! {
|
||||
impl #impl_generics vanth::Vanth for #name #ty_generics #where_clause {
|
||||
fn ty() -> vanth::Ty {
|
||||
impl #impl_generics #vanth_path::Vanth for #name #ty_generics #where_clause {
|
||||
fn ty() -> #vanth_path::Ty {
|
||||
let module_path = module_path!();
|
||||
let mut path: Vec<String> = module_path.split("::").map(|s| s.to_string()).collect();
|
||||
let base_name = stringify!(#name);
|
||||
let generics_str = #generics_code;
|
||||
path.push(format!("{}{}", base_name, generics_str));
|
||||
vanth::Ty { path }
|
||||
#vanth_path::Ty { path }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
25
crates/vanth_transport/Cargo.toml
Normal file
25
crates/vanth_transport/Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "vanth_transport"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
digest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
blake3.workspace = true
|
||||
vanth = { path = "../vanth" }
|
||||
vanth_derive = { path = "../vanth_derive" }
|
||||
tracing.workspace = true
|
||||
rand = { workspace = true }
|
||||
smol.workspace = true
|
||||
async-process.workspace = true
|
||||
async-trait.workspace = true
|
||||
ntrulp = { path = "/home/asraelite/dev/ntrulp", features = ["ntrup1277"] }
|
||||
chacha20poly1305.workspace = true
|
||||
serde_bytes.workspace = true
|
||||
serde_arrays.workspace = true
|
26
crates/vanth_transport/src/crypto.rs
Normal file
26
crates/vanth_transport/src/crypto.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use vanth::Vanth;
|
||||
|
||||
pub mod ntru;
|
||||
|
||||
pub enum CryptoError {
|
||||
RngGenerationFailed,
|
||||
InvalidKey,
|
||||
}
|
||||
|
||||
pub trait Asymmetric {
|
||||
type PublicKey: Vanth + Serialize + DeserializeOwned;
|
||||
type PrivateKey: Vanth + Serialize + DeserializeOwned;
|
||||
type Seed;
|
||||
|
||||
fn generate_default_public_key(&mut self, private_key: &Self::PrivateKey) -> Result<Self::PublicKey, CryptoError>;
|
||||
fn generate_private_key(&mut self, seed: Self::Seed) -> Result<Self::PrivateKey, CryptoError>;
|
||||
}
|
||||
|
||||
pub trait MultiKeyAsymmetric: Asymmetric {
|
||||
fn generate_public_key(
|
||||
&mut self,
|
||||
seed: Self::Seed,
|
||||
private_key: &Self::PrivateKey,
|
||||
) -> Result<Self::PublicKey, CryptoError>;
|
||||
}
|
151
crates/vanth_transport/src/crypto/ntru.rs
Normal file
151
crates/vanth_transport/src/crypto/ntru.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
use super::{Asymmetric, CryptoError};
|
||||
use ntrulp::{
|
||||
key::{priv_key::PrivKey, pub_key::PubKey},
|
||||
poly::{r3::R3, rq::Rq},
|
||||
};
|
||||
use serde::{Deserialize, Serialize, ser::SerializeStruct as _};
|
||||
use vanth::Vanth;
|
||||
|
||||
const NTRU_COEFFS_LEN: usize = 1277;
|
||||
|
||||
pub struct Ntru {}
|
||||
|
||||
#[derive(Vanth)]
|
||||
pub struct PrivateKey(PrivKey);
|
||||
|
||||
impl From<PrivKey> for PrivateKey {
|
||||
fn from(value: PrivKey) -> Self {
|
||||
PrivateKey(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PrivateKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_struct("PrivateKey", 2)?;
|
||||
s.serialize_field("f", &self.0.0.coeffs.to_vec())?;
|
||||
s.serialize_field("g", &self.0.1.coeffs.to_vec())?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PrivateKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct PrivateKeyHelper {
|
||||
f: Vec<i8>,
|
||||
g: Vec<i8>,
|
||||
}
|
||||
|
||||
let helper = PrivateKeyHelper::deserialize(deserializer)?;
|
||||
|
||||
let f = {
|
||||
let slice = helper.f.as_slice();
|
||||
if slice.len() != NTRU_COEFFS_LEN {
|
||||
return Err(serde::de::Error::invalid_length(
|
||||
slice.len(),
|
||||
&"invalid coeffs length for PrivateKey.f",
|
||||
));
|
||||
}
|
||||
let mut coeffs = [0i8; NTRU_COEFFS_LEN];
|
||||
coeffs.copy_from_slice(slice);
|
||||
R3 { coeffs }
|
||||
};
|
||||
let g = {
|
||||
let slice = helper.g.as_slice();
|
||||
if slice.len() != NTRU_COEFFS_LEN {
|
||||
return Err(serde::de::Error::invalid_length(
|
||||
slice.len(),
|
||||
&"invalid coeffs length for PrivateKey.g",
|
||||
));
|
||||
}
|
||||
let mut coeffs = [0i8; NTRU_COEFFS_LEN];
|
||||
coeffs.copy_from_slice(slice);
|
||||
R3 { coeffs }
|
||||
};
|
||||
|
||||
Ok(PrivateKey(PrivKey(f, g)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Vanth)]
|
||||
pub struct PublicKey(PubKey);
|
||||
|
||||
impl From<PubKey> for PublicKey {
|
||||
fn from(value: PubKey) -> Self {
|
||||
PublicKey(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_struct("PublicKey", 1)?;
|
||||
s.serialize_field("coeffs", &self.0.coeffs.to_vec())?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PublicKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct PublicKeyHelper {
|
||||
coeffs: Vec<i16>,
|
||||
}
|
||||
|
||||
let helper = PublicKeyHelper::deserialize(deserializer)?;
|
||||
let slice = helper.coeffs.as_slice();
|
||||
if slice.len() != NTRU_COEFFS_LEN {
|
||||
return Err(serde::de::Error::invalid_length(
|
||||
slice.len(),
|
||||
&"invalid coeffs length for PublicKey.coeffs",
|
||||
));
|
||||
}
|
||||
let mut coeffs = [0i16; NTRU_COEFFS_LEN];
|
||||
coeffs.copy_from_slice(slice);
|
||||
Ok(PublicKey(Rq { coeffs }))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use seed during generation, or in some other way make the random number generator more generic.
|
||||
impl Asymmetric for Ntru {
|
||||
type PrivateKey = PrivateKey;
|
||||
type PublicKey = PublicKey;
|
||||
// TODO: Use correct type
|
||||
type Seed = u64;
|
||||
|
||||
fn generate_default_public_key(&mut self, private_key: &Self::PrivateKey) -> Result<Self::PublicKey, CryptoError> {
|
||||
let pubkey = PubKey::from_sk(&private_key.0).map_err(|_| CryptoError::InvalidKey)?;
|
||||
Ok(PublicKey(pubkey))
|
||||
}
|
||||
|
||||
fn generate_private_key(&mut self, seed: Self::Seed) -> Result<Self::PrivateKey, CryptoError> {
|
||||
let mut rng = rand::rng();
|
||||
let f: Rq = Rq::from(ntrulp::rng::short_random(&mut rng).unwrap());
|
||||
let mut g: R3;
|
||||
let mut attempt_count = 0;
|
||||
let sk: PrivKey = loop {
|
||||
g = R3::from(ntrulp::rng::random_small(&mut rng));
|
||||
|
||||
if let Ok(s) = PrivKey::compute(&f, &g) {
|
||||
break s;
|
||||
};
|
||||
|
||||
attempt_count += 1;
|
||||
if attempt_count > 100 {
|
||||
return Err(CryptoError::RngGenerationFailed);
|
||||
}
|
||||
};
|
||||
Ok(PrivateKey(sk))
|
||||
}
|
||||
}
|
44
crates/vanth_transport/src/lib.rs
Normal file
44
crates/vanth_transport/src/lib.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use smol::net::UdpSocket;
|
||||
use vanth::{
|
||||
entity::Id,
|
||||
network::{Address, Backend},
|
||||
};
|
||||
|
||||
pub mod crypto;
|
||||
|
||||
pub struct UdpBackend {
|
||||
open_sockets: HashMap<Id<UdpSocket>, UdpSocket>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Backend for UdpBackend {
|
||||
type ConnectionHandle = Id<UdpSocket>;
|
||||
type Error = String;
|
||||
|
||||
async fn connect<'a>(&mut self, address: &Address) -> Result<Self::ConnectionHandle, Self::Error> {
|
||||
let socket = match address {
|
||||
Address::Uri(uri) => UdpSocket::bind(uri).await.map_err(|e| e.to_string())?,
|
||||
_ => return Err(format!("Address {:?} is not supported for the UDP backend.", address)),
|
||||
};
|
||||
let id = Id::random();
|
||||
assert!(self.open_sockets.insert(id, socket).is_none());
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
async fn close(&mut self, connection: Self::ConnectionHandle) -> Result<(), Self::Error> {
|
||||
todo!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn poll(&mut self) -> impl Iterator<Item = Result<(Self::ConnectionHandle, Vec<u8>), Self::Error>> {
|
||||
todo!();
|
||||
[].into_iter()
|
||||
}
|
||||
|
||||
async fn send(&mut self, data: &[u8]) -> Result<(), Self::Error> {
|
||||
todo!()
|
||||
}
|
||||
}
|
50
crates/vanth_transport/tests/ntru_serde_roundtrip.rs
Normal file
50
crates/vanth_transport/tests/ntru_serde_roundtrip.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use serde_json::{from_value, to_value, Value};
|
||||
use vanth_transport::crypto::ntru::{PrivateKey, PublicKey};
|
||||
|
||||
use ntrulp::{
|
||||
key::{priv_key::PrivKey as NtruPrivKey, pub_key::PubKey as NtruPubKey},
|
||||
poly::{r3::R3, rq::Rq},
|
||||
};
|
||||
|
||||
fn generate_keypair() -> (NtruPrivKey, NtruPubKey) {
|
||||
let mut rng = rand::rng();
|
||||
let f: Rq = Rq::from(ntrulp::rng::short_random(&mut rng).expect("failed to sample short_random"));
|
||||
let mut g: R3;
|
||||
|
||||
let sk = loop {
|
||||
g = R3::from(ntrulp::rng::random_small(&mut rng));
|
||||
match NtruPrivKey::compute(&f, &g) {
|
||||
Ok(s) => break s,
|
||||
Err(_) => continue,
|
||||
};
|
||||
};
|
||||
|
||||
let pk = NtruPubKey::from_sk(&sk).expect("failed to derive public key from secret key");
|
||||
(sk, pk)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_private_key_serde_roundtrip() {
|
||||
let (sk, _) = generate_keypair();
|
||||
|
||||
// Round-trip via serde_json Value without touching internal fields.
|
||||
let wrapped: PrivateKey = sk.into();
|
||||
let json1: Value = to_value(&wrapped).expect("failed to serialize PrivateKey");
|
||||
let reparsed: PrivateKey = from_value(json1.clone()).expect("failed to deserialize PrivateKey");
|
||||
let json2: Value = to_value(&reparsed).expect("failed to serialize PrivateKey again");
|
||||
|
||||
assert_eq!(json2, json1, "PrivateKey JSON did not round-trip losslessly.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_public_key_serde_roundtrip() {
|
||||
let (_, pk) = generate_keypair();
|
||||
|
||||
// Round-trip via serde_json Value without touching internal fields.
|
||||
let wrapped: PublicKey = pk.into();
|
||||
let json1: Value = to_value(&wrapped).expect("failed to serialize PublicKey");
|
||||
let reparsed: PublicKey = from_value(json1.clone()).expect("failed to deserialize PublicKey");
|
||||
let json2: Value = to_value(&reparsed).expect("failed to serialize PublicKey again");
|
||||
|
||||
assert_eq!(json2, json1, "PublicKey JSON did not round-trip losslessly.");
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue