Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lang): add members to world store and re added model tests #2611

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/dojo/core-cairo-test/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ mod tests {
mod event;
}

// mod model {
// mod model;
// }
mod model {
mod model;
}

mod storage {
mod database;
Expand Down
105 changes: 31 additions & 74 deletions crates/dojo/core-cairo-test/src/tests/model/model.cairo
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use dojo::model::{Model, ModelValue, ModelStorage, ModelValueStorage, ModelMemberStorage};
use dojo::world::{IWorldDispatcherTrait, WorldStorageTrait, WorldStorage};

use crate::tests::helpers::{deploy_world};
use dojo::model::{Model, ModelValue, ModelStorage, ModelValueStorage, ModelPtr};
use dojo::world::WorldStorage;
use dojo_cairo_test::{spawn_test_world, NamespaceDef, TestResource};

#[derive(Copy, Drop, Serde, Debug)]
#[dojo::model]
Expand All @@ -26,6 +25,19 @@ struct Foo2 {
v2: u32
}

fn namespace_def() -> NamespaceDef {
NamespaceDef {
namespace: "dojo_cairo_test", resources: [
TestResource::Model(m_Foo::TEST_CLASS_HASH.try_into().unwrap()),
TestResource::Model(m_Foo2::TEST_CLASS_HASH.try_into().unwrap()),
].span()
}
}

fn spawn_foo_world() -> WorldStorage {
spawn_test_world([namespace_def()].span())
}

#[test]
fn test_model_definition() {
let definition = dojo::model::Model::<Foo>::definition();
Expand Down Expand Up @@ -65,73 +77,61 @@ fn test_from_values_bad_data() {
}

#[test]
fn test_get_and_update_model_value() {
let world = deploy_world();
world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap());

let mut world = WorldStorageTrait::new(world, "dojo");
fn test_read_and_update_model_value() {
let mut world = spawn_foo_world();

let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
world.write_model(@foo);

let entity_id = foo.entity_id();
let mut model_value: FooValue = world.read_model_value(foo.key());
let mut model_value: FooValue = world.read_value(foo.key());
assert_eq!(model_value.v1, foo.v1);
assert_eq!(model_value.v2, foo.v2);

model_value.v1 = 12;
model_value.v2 = 18;

world.write_model_value_from_id(entity_id, @model_value);
world.write_value_from_id(entity_id, @model_value);

let read_values: FooValue = world.read_model_value(foo.key());
let read_values: FooValue = world.read_value(foo.key());
assert!(read_values.v1 == model_value.v1 && read_values.v2 == model_value.v2);
}

#[test]
fn test_delete_model_value() {
let world = deploy_world();
world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap());

let mut world = WorldStorageTrait::new(world, "dojo");
let mut world = spawn_foo_world();

let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
world.write_model(@foo);

let entity_id = foo.entity_id();
ModelStorage::<WorldStorage, Foo>::erase_model(ref world, @foo);

let read_values: FooValue = world.read_model_value_from_id(entity_id);
let read_values: FooValue = world.read_value_from_id(entity_id);
assert!(read_values.v1 == 0 && read_values.v2 == 0);
}

#[test]
fn test_get_and_set_field_name() {
let world = deploy_world();
world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap());

let mut world = WorldStorageTrait::new(world, "dojo");
fn test_read_and_write_field_name() {
let mut world = spawn_foo_world();

let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
world.write_model(@foo);

// Inference fails here, we need something better without too generics
// which also fails.
let v1 = world.read_member(foo.key(), selector!("v1"));
let v1 = world.read_member(ModelPtr::<Foo>::Id(foo.entity_id()), selector!("v1"));
assert!(foo.v1 == v1);

world.write_member_from_id(foo.entity_id(), selector!("v1"), 42);
world.write_member(ModelPtr::<Foo>::Id(foo.entity_id()), selector!("v1"), 42);

let v1 = world.read_member_from_id(foo.key(), selector!("v1"));
let v1 = world.read_member(ModelPtr::<Foo>::Keys(foo.keys()), selector!("v1"));
assert!(v1 == 42);
}

#[test]
fn test_get_and_set_from_model() {
let world = deploy_world();
world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap());

let mut world = WorldStorageTrait::new(world, "dojo");
fn test_read_and_write_from_model() {
let mut world = spawn_foo_world();

let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
world.write_model(@foo);
Expand All @@ -143,10 +143,7 @@ fn test_get_and_set_from_model() {

#[test]
fn test_delete_from_model() {
let world = deploy_world();
world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap());

let mut world = WorldStorageTrait::new(world, "dojo");
let mut world = spawn_foo_world();

let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
world.write_model(@foo);
Expand All @@ -155,43 +152,3 @@ fn test_delete_from_model() {
let foo2: Foo = world.read_model((foo.k1, foo.k2));
assert!(foo2.k1 == foo.k1 && foo2.k2 == foo.k2 && foo2.v1 == 0 && foo2.v2 == 0);
}

#[test]
fn test_get_and_set_member_from_model() {
let world = deploy_world();
world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap());

let mut world = WorldStorageTrait::new(world, "dojo");

let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
world.write_model(@foo);

let key: (u8, felt252) = foo.key();
let v1: u128 = world.read_member(key, selector!("v1"));

assert!(v1 == 3);

world.write_member(key, selector!("v1"), 42);
let foo: Foo = world.read_model(key);
assert!(foo.v1 == 42);
}

#[test]
fn test_get_and_set_field_name_from_model() {
let world = deploy_world();
world.register_model("dojo", foo::TEST_CLASS_HASH.try_into().unwrap());

let mut world = WorldStorageTrait::new(world, "dojo");

let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
world.write_model(@foo);

// Currently we don't have automatic field id computation. To be done.
// @remy/@ben.

let v1 = world.read_member((foo.k1, foo.k2), selector!("v1"));
assert!(v1 == 3);

world.write_member((foo.k1, foo.k2), selector!("v1"), 42);
assert!(v1 == 42);
}
12 changes: 10 additions & 2 deletions crates/dojo/core/src/model/model.cairo
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use dojo::{meta::{Layout, introspect::Ty, layout::compute_packed_size}, utils::entity_id_from_keys};
use dojo::{
meta::{Layout, introspect::Ty, layout::compute_packed_size},
utils::{entity_id_from_keys, find_model_field_layout}
};

use super::{ModelDefinition, ModelDef};

/// Trait `KeyParser` defines a trait for parsing keys from a given model.
pub trait KeyParser<M, K> {
/// Parses the key from the given model.
Expand Down Expand Up @@ -39,6 +41,8 @@ pub trait Model<M> {
fn schema() -> Ty;
/// Returns the memory layout of the model.
fn layout() -> Layout;
/// Returns the layout of a field in the model.
fn field_layout(field_selector: felt252) -> Option<Layout>;
/// Returns the unpacked size of the model. Only applicable for fixed size models.
fn unpacked_size() -> Option<usize>;
/// Returns the packed size of the model. Only applicable for fixed size models.
Expand Down Expand Up @@ -92,6 +96,10 @@ pub impl ModelImpl<M, +ModelParser<M>, +ModelDefinition<M>, +Serde<M>> of Model<
ModelDefinition::<M>::layout()
}

fn field_layout(field_selector: felt252) -> Option<Layout> {
find_model_field_layout(Self::layout(), field_selector)
}

fn schema() -> Ty {
ModelDefinition::<M>::schema()
}
Expand Down
28 changes: 23 additions & 5 deletions crates/dojo/core/src/model/storage.cairo
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use dojo::model::model_value::ModelValueKey;
use dojo::{model::model_value::ModelValueKey, utils::entity_id_from_keys};

// TODO: define the right interface for member accesses.

Expand All @@ -12,24 +12,42 @@ pub enum ModelPtr<M> {
Keys: Span<felt252>,
}


pub trait ModelPtrTrait<M> {
/// Returns the entity id of the model.
fn entity_id(self: @ModelPtr<M>) -> felt252;
}

pub impl ModelPtrImpl<M> of ModelPtrTrait<M> {
/// Returns the entity id of the model.
fn entity_id(self: @ModelPtr<M>) -> felt252 {
match self {
ModelPtr::Id(id) => *id,
ModelPtr::Keys(keys) => entity_id_from_keys(*keys),
}
}
}

/// A `ModelStorage` trait that abstracts where the storage is.
///
/// Currently it's only world storage, but this will be useful when we have other
/// storage solutions (micro worlds).
pub trait ModelStorage<S, M> {
/// Sets a model of type `M`.
fn write_model(ref self: S, model: @M);

/// Retrieves a model of type `M` using the provided key of type `K`.
fn read_model<K, +Drop<K>, +Serde<K>>(self: @S, key: K) -> M;

/// Deletes a model of type `M`.
fn erase_model(ref self: S, model: @M);

/// Deletes a model of type `M` using the provided entity id.
/// The ptr is mostly used for type inferrence.
fn erase_model_ptr(ref self: S, ptr: ModelPtr<M>);

/// Retrieves a model of type `M` using the provided entity idref .
fn read_member<T, +Serde<T>>(self: @S, ptr: ModelPtr<M>, field_selector: felt252) -> T;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Retrieves a model of type `M` using the provided entity idref .
/// Retrieves a model of type `M` using the provided entity id.

/// Retrieves a model of type `M` using the provided entity id.
fn write_member<T, +Serde<T>, +Drop<T>>(
ref self: S, ptr: ModelPtr<M>, field_selector: felt252, value: T
);
/// Returns the current namespace hash.
Comment on lines +45 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct the documentation comments to match the methods' functionality

Ohayo, sensei! The documentation comments for read_member and write_member don't accurately reflect their purposes. Please update them to describe that read_member retrieves a member of a model, and write_member updates a member of a model.

Apply this diff to correct the documentation comments:

-    /// Retrieves a model of type `M` using the provided entity idref .
+    /// Retrieves a member of a model of type `M` using the provided field selector.

     fn read_member<T, +Serde<T>>(self: @S, ptr: ModelPtr<M>, field_selector: felt252) -> T;

-    /// Retrieves a model of type `M` using the provided entity id.
+    /// Updates a member of a model of type `M` using the provided field selector.

     fn write_member<T, +Serde<T>, +Drop<T>>(
         ref self: S, ptr: ModelPtr<M>, field_selector: felt252, value: T
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Retrieves a model of type `M` using the provided entity idref .
fn read_member<T, +Serde<T>>(self: @S, ptr: ModelPtr<M>, field_selector: felt252) -> T;
/// Retrieves a model of type `M` using the provided entity id.
fn write_member<T, +Serde<T>, +Drop<T>>(
ref self: S, ptr: ModelPtr<M>, field_selector: felt252, value: T
);
/// Retrieves a member of a model of type `M` using the provided field selector.
fn read_member<T, +Serde<T>>(self: @S, ptr: ModelPtr<M>, field_selector: felt252) -> T;
/// Updates a member of a model of type `M` using the provided field selector.
fn write_member<T, +Serde<T>, +Drop<T>>(
ref self: S, ptr: ModelPtr<M>, field_selector: felt252, value: T
);

fn namespace_hash(self: @S) -> felt252;
}
Expand Down
37 changes: 35 additions & 2 deletions crates/dojo/core/src/world/storage.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

use core::panic_with_felt252;
use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, Resource};
use dojo::model::{Model, ModelIndex, ModelValueKey, ModelValue, ModelStorage, ModelPtr};
use dojo::model::{
Model, ModelIndex, ModelValueKey, ModelValue, ModelStorage, ModelPtr, storage::ModelPtrTrait
};
use dojo::event::{Event, EventStorage};
use dojo::meta::Layout;
use dojo::utils::{
entity_id_from_key, entity_id_from_keys, serialize_inline, find_model_field_layout
entity_id_from_key, entity_id_from_keys, serialize_inline, find_model_field_layout,
deserialize_unwrap
};
use starknet::{ContractAddress, ClassHash};

Expand All @@ -16,6 +19,13 @@ pub struct WorldStorage {
pub namespace_hash: felt252,
}

fn field_layout_unwrap<M, +Model<M>>(field_selector: felt252) -> Layout {
match Model::<M>::field_layout(field_selector) {
Option::Some(layout) => layout,
Option::None => panic_with_felt252('bad member id')
}
}

#[generate_trait]
pub impl WorldStorageInternalImpl of WorldStorageTrait {
fn new(world: IWorldDispatcher, namespace: @ByteArray) -> WorldStorage {
Expand Down Expand Up @@ -108,6 +118,29 @@ pub impl ModelStorageWorldStorageImpl<M, +Model<M>, +Drop<M>> of ModelStorage<Wo
Model::<M>::layout()
);
}
fn read_member<T, +Serde<T>>(
self: @WorldStorage, ptr: ModelPtr<M>, field_selector: felt252
) -> T {
deserialize_unwrap(
IWorldDispatcherTrait::entity(
*self.dispatcher,
Model::<M>::selector(*self.namespace_hash),
ModelIndex::MemberId((ptr.entity_id(), field_selector)),
field_layout_unwrap::<M>(field_selector)
)
)
}
Comment on lines +121 to +132
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle deserialization errors in read_member more robustly.

Ohayo, sensei! Currently, read_member uses deserialize_unwrap, which panics if deserialization fails. Consider handling deserialization errors more gracefully to avoid unexpected panics.

You could modify the code to handle errors explicitly:

 fn read_member<T, +Serde<T>>(
     self: @WorldStorage, ptr: ModelPtr<M>, field_selector: felt252
 ) -> T {
-    deserialize_unwrap(
+    match deserialize(
         IWorldDispatcherTrait::entity(
             *self.dispatcher,
             Model::<M>::selector(*self.namespace_hash),
             ModelIndex::MemberId((ptr.entity_id(), field_selector)),
             field_layout_unwrap::<M>(field_selector)
-        )
+        )
+    ) {
+        Option::Some(value) => value,
+        Option::None => panic_with_felt252('Deserialization failed for read_member')
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn read_member<T, +Serde<T>>(
self: @WorldStorage, ptr: ModelPtr<M>, field_selector: felt252
) -> T {
deserialize_unwrap(
IWorldDispatcherTrait::entity(
*self.dispatcher,
Model::<M>::selector(*self.namespace_hash),
ModelIndex::MemberId((ptr.entity_id(), field_selector)),
field_layout_unwrap::<M>(field_selector)
)
)
}
fn read_member<T, +Serde<T>>(
self: @WorldStorage, ptr: ModelPtr<M>, field_selector: felt252
) -> T {
match deserialize(
IWorldDispatcherTrait::entity(
*self.dispatcher,
Model::<M>::selector(*self.namespace_hash),
ModelIndex::MemberId((ptr.entity_id(), field_selector)),
field_layout_unwrap::<M>(field_selector)
)
) {
Option::Some(value) => value,
Option::None => panic_with_felt252('Deserialization failed for read_member')
}
}

fn write_member<T, +Serde<T>, +Drop<T>>(
ref self: WorldStorage, ptr: ModelPtr<M>, field_selector: felt252, value: T
) {
IWorldDispatcherTrait::set_entity(
self.dispatcher,
Model::<M>::selector(self.namespace_hash),
ModelIndex::MemberId((ptr.entity_id(), field_selector)),
serialize_inline(@value),
field_layout_unwrap::<M>(field_selector)
);
}
Comment on lines +133 to +143
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure error handling in write_member when the field layout is not found.

Ohayo, sensei! The write_member function relies on field_layout_unwrap, which panics if the field layout is not found. Consider handling this error case to improve robustness.

You might adjust the code to handle the None case explicitly:

 fn write_member<T, +Serde<T>, +Drop<T>>(
     ref self: WorldStorage, ptr: ModelPtr<M>, field_selector: felt252, value: T
 ) {
+    let layout_option = Model::<M>::field_layout(field_selector);
+    match layout_option {
+        Option::Some(layout) => {
         IWorldDispatcherTrait::set_entity(
             self.dispatcher,
             Model::<M>::selector(self.namespace_hash),
             ModelIndex::MemberId((ptr.entity_id(), field_selector)),
             serialize_inline(@value),
-            field_layout_unwrap::<M>(field_selector)
+            layout
         );
+        },
+        Option::None => panic_with_felt252('Field layout not found in write_member')
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn write_member<T, +Serde<T>, +Drop<T>>(
ref self: WorldStorage, ptr: ModelPtr<M>, field_selector: felt252, value: T
) {
IWorldDispatcherTrait::set_entity(
self.dispatcher,
Model::<M>::selector(self.namespace_hash),
ModelIndex::MemberId((ptr.entity_id(), field_selector)),
serialize_inline(@value),
field_layout_unwrap::<M>(field_selector)
);
}
fn write_member<T, +Serde<T>, +Drop<T>>(
ref self: WorldStorage, ptr: ModelPtr<M>, field_selector: felt252, value: T
) {
let layout_option = Model::<M>::field_layout(field_selector);
match layout_option {
Option::Some(layout) => {
IWorldDispatcherTrait::set_entity(
self.dispatcher,
Model::<M>::selector(self.namespace_hash),
ModelIndex::MemberId((ptr.entity_id(), field_selector)),
serialize_inline(@value),
layout
);
},
Option::None => panic_with_felt252('Field layout not found in write_member')
}
}


fn namespace_hash(self: @WorldStorage) -> felt252 {
*self.namespace_hash
Expand Down
Loading