♻️🍱💩👷 crates/vanth, project: Refactor entity system and setup; migrate to Nix flakes
- Add `.cargo/config.toml` with target directory and rustflags configuration. - Remove devenv files and set up Nix flake configuration with `flake.nix` and `flake.lock`. - Add dependencies on `blake3`, `digest`, and hashing-related crates for content hashing functionality. - Implement `EntityId` as hashed representation and `ContentHash` component using Blake3. - Add `hashing_serializer` module for serializing and hashing components. - Remove unused `parser` module and restructure lib.rs with new `Reference` type and storage interfaces. - Add test files for `store` and `reference` functionality with TODOs for implementation. - Introduce `VanthPlugin` skeleton and entity save/load interfaces with placeholder implementations. - Add `Reference` type with retrieval state machine and async handling stubs.
This commit is contained in:
parent
be75844fdd
commit
45a68865b6
19 changed files with 918 additions and 261 deletions
|
@ -9,5 +9,7 @@ path = "src/lib.rs"
|
|||
[dependencies]
|
||||
bevy_app.workspace = true
|
||||
bevy_ecs.workspace = true
|
||||
digest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
blake3.workspace = true
|
||||
|
|
|
@ -3,6 +3,18 @@ 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]);
|
||||
|
||||
impl From<String> for EntityId {
|
||||
fn from(value: String) -> Self {
|
||||
Self(hash(&value).hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic identifier type that can be used for different entity types
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Id<T: ?Sized> {
|
||||
|
|
390
crates/vanth/src/hashing_serializer.rs
Normal file
390
crates/vanth/src/hashing_serializer.rs
Normal file
|
@ -0,0 +1,390 @@
|
|||
// Stolen and adapted from https://github.com/fjarri/hashing-serializer/blob/master/src/lib.rs
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use digest::Update;
|
||||
use serde::{
|
||||
ser::{
|
||||
self, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
|
||||
SerializeTupleStruct, SerializeTupleVariant,
|
||||
},
|
||||
Serialize, Serializer,
|
||||
};
|
||||
|
||||
use crate::ContentHash;
|
||||
|
||||
pub fn hash(value: &impl Serialize) -> ContentHash {
|
||||
let mut digest = blake3::Hasher::new();
|
||||
let mut serializer = HashingSerializer { digest: &mut digest };
|
||||
// TODO: Don't unwrap.
|
||||
serializer.serialize_value(value).unwrap();
|
||||
ContentHash { hash: *serializer.digest.finalize().as_bytes() }
|
||||
}
|
||||
|
||||
/// A serializer that hashes the data instead of serializing it.
|
||||
pub struct HashingSerializer<'a, T: Update> {
|
||||
/// A reference to the digest that will accumulate the data.
|
||||
pub digest: &'a mut T,
|
||||
}
|
||||
|
||||
/// Possible errors during serialization.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Error {
|
||||
/// The type's [`serde::Serialize`] impl tried to serialize a sequence of undefined length.
|
||||
UndefinedSequenceLength,
|
||||
|
||||
/// Sequence length does not fit into `u128`.
|
||||
///
|
||||
/// Really, this shouldn't ever happen.
|
||||
SequenceLengthTooLarge,
|
||||
|
||||
/// [`Serializer::collect_str`] got called, but heap memory allocation is not available.
|
||||
///
|
||||
/// Can only be returned if `alloc` feature not enabled.
|
||||
CannotCollectStr,
|
||||
|
||||
/// Custom `serde` error, but memory allocation is not available.
|
||||
/// Set a breakpoint where this is thrown for more information.
|
||||
///
|
||||
/// Can only be returned if `alloc` feature not enabled).
|
||||
CustomErrorNoAlloc,
|
||||
|
||||
/// Custom `serde` error.
|
||||
CustomError(String),
|
||||
}
|
||||
|
||||
impl ser::Error for Error {
|
||||
fn custom<T: fmt::Display>(msg: T) -> Self {
|
||||
Self::CustomError(format!("{}", msg))
|
||||
}
|
||||
}
|
||||
|
||||
impl ser::StdError for Error {}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the `usize` sequence length to a fixed length type,
|
||||
/// since we want the result to be portable.
|
||||
fn try_into_sequence_length(len: usize) -> Result<u128, Error> {
|
||||
u128::try_from(len)
|
||||
.ok()
|
||||
.ok_or(Error::SequenceLengthTooLarge)
|
||||
}
|
||||
|
||||
// Implement `serialize_$ty` for int types
|
||||
macro_rules! impl_trivial_serialize {
|
||||
($method_name:ident , $t:ty) => {
|
||||
fn $method_name(self, v: $t) -> Result<Self::Ok, Self::Error> {
|
||||
self.digest.update(&v.to_be_bytes());
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_serialize_trait {
|
||||
($method_name:ident , $t:ty) => {
|
||||
fn $method_name(self, v: $t) -> Result<Self::Ok, Self::Error> {
|
||||
self.digest.update(&v.to_be_bytes());
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<'a, T: Update> Serializer for HashingSerializer<'a, T> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
type SerializeSeq = Self;
|
||||
type SerializeTuple = Self;
|
||||
type SerializeTupleStruct = Self;
|
||||
type SerializeTupleVariant = Self;
|
||||
type SerializeMap = Self;
|
||||
type SerializeStruct = Self;
|
||||
type SerializeStructVariant = Self;
|
||||
|
||||
fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> {
|
||||
self.digest.update(&(v as u8).to_be_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_trivial_serialize!(serialize_i8, i8);
|
||||
impl_trivial_serialize!(serialize_i16, i16);
|
||||
impl_trivial_serialize!(serialize_i32, i32);
|
||||
impl_trivial_serialize!(serialize_i64, i64);
|
||||
|
||||
impl_trivial_serialize!(serialize_u8, u8);
|
||||
impl_trivial_serialize!(serialize_u16, u16);
|
||||
impl_trivial_serialize!(serialize_u32, u32);
|
||||
impl_trivial_serialize!(serialize_u64, u64);
|
||||
|
||||
impl_trivial_serialize!(serialize_f32, f32);
|
||||
impl_trivial_serialize!(serialize_f64, f64);
|
||||
|
||||
fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> {
|
||||
// `char` is always at most 4 bytes, regardless of the platform,
|
||||
// so this conversion is safe.
|
||||
self.digest.update(&u64::from(v).to_be_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
|
||||
self.digest.update(v.as_ref());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.digest.update(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
|
||||
self.digest.update(&[0]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_some<V: ?Sized + Serialize>(self, value: &V) -> Result<Self::Ok, Self::Error> {
|
||||
self.digest.update(&[1]);
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_unit_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
variant_index: u32,
|
||||
_variant: &'static str,
|
||||
) -> Result<Self::Ok, Self::Error> {
|
||||
self.digest.update(&variant_index.to_be_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_newtype_struct<V: ?Sized + Serialize>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
value: &V,
|
||||
) -> Result<Self::Ok, Self::Error> {
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_newtype_variant<V: ?Sized + Serialize>(
|
||||
self,
|
||||
_name: &'static str,
|
||||
variant_index: u32,
|
||||
_variant: &'static str,
|
||||
value: &V,
|
||||
) -> Result<Self::Ok, Self::Error> {
|
||||
self.digest.update(&variant_index.to_be_bytes());
|
||||
value.serialize(self)
|
||||
}
|
||||
|
||||
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
|
||||
let len = len.ok_or(Error::UndefinedSequenceLength)?;
|
||||
self.digest
|
||||
.update(&try_into_sequence_length(len)?.to_be_bytes());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
|
||||
self.digest
|
||||
.update(&try_into_sequence_length(len)?.to_be_bytes());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn serialize_tuple_struct(
|
||||
self,
|
||||
_name: &'static str,
|
||||
len: usize,
|
||||
) -> Result<Self::SerializeTupleStruct, Self::Error> {
|
||||
self.digest
|
||||
.update(&try_into_sequence_length(len)?.to_be_bytes());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
variant_index: u32,
|
||||
_variant: &'static str,
|
||||
len: usize,
|
||||
) -> Result<Self::SerializeTupleVariant, Self::Error> {
|
||||
self.digest.update(&variant_index.to_be_bytes());
|
||||
self.digest
|
||||
.update(&try_into_sequence_length(len)?.to_be_bytes());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
|
||||
let len = len.ok_or(Error::UndefinedSequenceLength)?;
|
||||
self.digest
|
||||
.update(&try_into_sequence_length(len)?.to_be_bytes());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn serialize_struct(
|
||||
self,
|
||||
_name: &'static str,
|
||||
len: usize,
|
||||
) -> Result<Self::SerializeStruct, Self::Error> {
|
||||
self.digest
|
||||
.update(&try_into_sequence_length(len)?.to_be_bytes());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_name: &'static str,
|
||||
variant_index: u32,
|
||||
_variant: &'static str,
|
||||
len: usize,
|
||||
) -> Result<Self::SerializeStructVariant, Self::Error> {
|
||||
self.digest.update(&variant_index.to_be_bytes());
|
||||
self.digest
|
||||
.update(&try_into_sequence_length(len)?.to_be_bytes());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn collect_str<V: fmt::Display + ?Sized>(self, _: &V) -> Result<Self::Ok, Self::Error> {
|
||||
Err(Error::CannotCollectStr)
|
||||
}
|
||||
|
||||
fn is_human_readable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Update> SerializeSeq for HashingSerializer<'a, T> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_element<V: ?Sized + Serialize>(&mut self, value: &V) -> Result<Self::Ok, Error> {
|
||||
value.serialize(HashingSerializer {
|
||||
digest: self.digest,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Update> SerializeTuple for HashingSerializer<'a, T> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_element<V: ?Sized + Serialize>(&mut self, value: &V) -> Result<Self::Ok, Error> {
|
||||
value.serialize(HashingSerializer {
|
||||
digest: self.digest,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Update> SerializeTupleStruct for HashingSerializer<'a, T> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<V: ?Sized + Serialize>(&mut self, value: &V) -> Result<Self::Ok, Error> {
|
||||
value.serialize(HashingSerializer {
|
||||
digest: self.digest,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Update> SerializeTupleVariant for HashingSerializer<'a, T> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<V: ?Sized + Serialize>(&mut self, value: &V) -> Result<Self::Ok, Error> {
|
||||
value.serialize(HashingSerializer {
|
||||
digest: self.digest,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Update> SerializeMap for HashingSerializer<'a, T> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_key<K: ?Sized + Serialize>(&mut self, key: &K) -> Result<Self::Ok, Error> {
|
||||
key.serialize(HashingSerializer {
|
||||
digest: self.digest,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_value<V: ?Sized + Serialize>(&mut self, value: &V) -> Result<Self::Ok, Error> {
|
||||
value.serialize(HashingSerializer {
|
||||
digest: self.digest,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Update> SerializeStruct for HashingSerializer<'a, T> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<V: ?Sized + Serialize>(
|
||||
&mut self,
|
||||
_key: &'static str,
|
||||
value: &V,
|
||||
) -> Result<Self::Ok, Error> {
|
||||
value.serialize(HashingSerializer {
|
||||
digest: self.digest,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Update> SerializeStructVariant for HashingSerializer<'a, T> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn serialize_field<V: ?Sized + Serialize>(
|
||||
&mut self,
|
||||
_key: &'static str,
|
||||
value: &V,
|
||||
) -> Result<Self::Ok, Error> {
|
||||
value.serialize(HashingSerializer {
|
||||
digest: self.digest,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end(self) -> Result<Self::Ok, Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,12 +1,23 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
/// Library crate for the `vanth` ECS-based database node.
|
||||
use bevy_app::App;
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_ecs::{prelude::*, query::QueryData};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::entity::EntityId;
|
||||
|
||||
pub mod store;
|
||||
pub mod entity;
|
||||
pub mod hashing_serializer;
|
||||
|
||||
pub use hashing_serializer::hash;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub enum Error {
|
||||
Other(String),
|
||||
}
|
||||
|
||||
/// A server node wrapping a Bevy App without running it.
|
||||
pub struct Node {
|
||||
|
@ -31,6 +42,16 @@ impl Node {
|
|||
pub fn run() {
|
||||
|
||||
}
|
||||
|
||||
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(Debug, Deserialize, Component, Serialize)]
|
||||
|
@ -50,13 +71,81 @@ pub trait VanthTuple {
|
|||
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct EntityContents {
|
||||
components: Vec<ComponentContents>
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ComponentContents {
|
||||
id: String,
|
||||
content_hash: ContentHash,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub trait Component: Serialize {
|
||||
fn id() -> String;
|
||||
}
|
||||
|
||||
// use a macro to implement VanthTuiple here.
|
||||
|
||||
#[derive(Debug, Deserialize, Component, Serialize)]
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Component, Serialize)]
|
||||
pub struct ContentHash {
|
||||
pub hash: [u8; 32],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Component, Serialize)]
|
||||
pub struct Reference<T: Clone + Serialize> {
|
||||
value: ReferenceValue,
|
||||
_marker: PhantomData<T>
|
||||
}
|
||||
|
||||
#[derive( Clone, Debug, Deserialize, Component, Serialize)]
|
||||
pub enum ReferenceValue {
|
||||
Absent,
|
||||
Retrieving(ReferenceRetrievalTask),
|
||||
Present(Vec<u8>)
|
||||
}
|
||||
|
||||
impl <T: Clone + Serialize> Reference<T> {
|
||||
pub async fn take() -> T {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub async fn get() -> Handle<T> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Deserialize, Serialize)]
|
||||
struct ReferenceRetrievalTask {
|
||||
|
||||
}
|
||||
|
||||
impl Future for ReferenceRetrievalTask {
|
||||
type Output = Vec<u8>;
|
||||
|
||||
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Handle<T> {
|
||||
_marker: PhantomData<T>
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// A trait is derivable for ECS components
|
||||
// The components must have a content hash, not the entity. For efficiency and ergonomics. This means that a hash of each relevant component must be stored in the Vanth component of the entity, in a `HashMap` or something. The ID of the component used by Vanth should be a method on the derived trait.
|
||||
|
||||
pub struct VanthPlugin;
|
||||
|
||||
impl Plugin for VanthPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// fn run_reference_tasks(tasks: Query<(&ReferenceGetTask<>)>) {
|
||||
|
||||
// }
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
#![allow(unused)]
|
||||
|
||||
mod parser;
|
||||
mod util;
|
||||
mod vanth;
|
||||
|
||||
|
|
|
@ -11,3 +11,20 @@ pub struct Store {
|
|||
impl Store {
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Component, Serialize)]
|
||||
pub struct Cache {
|
||||
size_limit_bytes: u64,
|
||||
backend: Backend,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum Backend {
|
||||
Memory,
|
||||
Sqlite(Sqlite)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Sqlite {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
|
28
crates/vanth/tests/reference.rs
Normal file
28
crates/vanth/tests/reference.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use vanth::{Component, Node, Reference};
|
||||
|
||||
// #[test]
|
||||
// fn test_store() {
|
||||
// #[derive(Clone, Deserialize, Serialize)]
|
||||
// struct Foo {
|
||||
// bar: Reference<Bar>,
|
||||
// }
|
||||
|
||||
// #[derive(Clone, Deserialize, Serialize)]
|
||||
// struct Bar {
|
||||
// foo: Reference<Foo>,
|
||||
// }
|
||||
|
||||
// impl Component for Foo {
|
||||
// fn id() -> String {
|
||||
// "foo".into()
|
||||
// }
|
||||
// }
|
||||
|
||||
// let node = Node::in_memory();
|
||||
|
||||
// let entity_id = "entity_1";
|
||||
// let entity_components = (Foo { a: 5, b: 6.0 },);
|
||||
|
||||
// node.save("entity_1", entity_components);
|
||||
// }
|
24
crates/vanth/tests/store.rs
Normal file
24
crates/vanth/tests/store.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use vanth::{Component, Node};
|
||||
|
||||
// #[test]
|
||||
// fn test_store() {
|
||||
// #[derive(Deserialize, Serialize)]
|
||||
// struct Foo {
|
||||
// a: u32,
|
||||
// b: f32,
|
||||
// }
|
||||
|
||||
// impl Component for Foo {
|
||||
// fn id() -> String {
|
||||
// "foo".into()
|
||||
// }
|
||||
// }
|
||||
|
||||
// let node = Node::in_memory();
|
||||
|
||||
// let entity_id = "entity_1";
|
||||
// let entity_components = (Foo { a: 5, b: 6.0 },);
|
||||
|
||||
// node.save("entity_1", entity_components);
|
||||
// }
|
Loading…
Add table
Add a link
Reference in a new issue