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(core): model get/set functions #2159

Merged
merged 4 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions crates/dojo-core/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ mod database;
mod database_test;
mod interfaces;
mod model;
#[cfg(test)]
mod model_test;
mod contract;
mod packing;
#[cfg(test)]
Expand Down
31 changes: 27 additions & 4 deletions crates/dojo-core/src/model.cairo
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
use dojo::world::IWorldDispatcher;
use dojo::world::{IWorldDispatcher, ModelIndex};
use starknet::SyscallResult;

/// Trait that is implemented at Cairo level for each struct that is a model.
trait ModelEntity<T> {
fn id(self: @T) -> felt252;
fn values(self: @T) -> Span<felt252>;
fn from_values(entity_id: felt252, values: Span<felt252>) -> T;
fn get(world: IWorldDispatcher, entity_id: felt252) -> T;
fn update(self: @T, world: IWorldDispatcher);
fn delete(self: @T, world: IWorldDispatcher);
fn get_member(
world: IWorldDispatcher, entity_id: felt252, member_id: felt252,
) -> Span<felt252>;
fn set_member(self: @T, world: IWorldDispatcher, member_id: felt252, values: Span<felt252>,);
}

trait Model<T> {
fn entity(
world: IWorldDispatcher, keys: Span<felt252>, layout: dojo::database::introspect::Layout
) -> T;
fn get(world: IWorldDispatcher, keys: Span<felt252>) -> T;
// Note: `get` is implemented with a generated trait because it takes
// the list of model keys as separated parameters.
fn set(self: @T, world: IWorldDispatcher);
fn delete(self: @T, world: IWorldDispatcher);

fn get_member(
world: IWorldDispatcher, keys: Span<felt252>, member_id: felt252,
) -> Span<felt252>;

fn set_member(self: @T, world: IWorldDispatcher, member_id: felt252, values: Span<felt252>,);

/// Returns the name of the model as it was written in Cairo code.
fn name() -> ByteArray;
Expand All @@ -25,6 +47,7 @@ trait Model<T> {
fn name_hash() -> felt252;
fn namespace_hash() -> felt252;

fn entity_id(self: @T) -> felt252;
fn keys(self: @T) -> Span<felt252>;
fn values(self: @T) -> Span<felt252>;
fn layout() -> dojo::database::introspect::Layout;
Expand Down
198 changes: 198 additions & 0 deletions crates/dojo-core/src/model_test.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use dojo::test_utils::{spawn_test_world};
use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait};

// Utils
fn deploy_world() -> IWorldDispatcher {
spawn_test_world("dojo", array![])
}

#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Foo {
#[key]
k1: u8,
#[key]
k2: felt252,
v1: u128,
v2: u32
}

#[test]
fn test_id() {
let mvalues = FooEntity { __id: 1, v1: 3, v2: 4 };
assert!(mvalues.id() == 1);
}

#[test]
fn test_values() {
let mvalues = FooEntity { __id: 1, v1: 3, v2: 4 };
let expected_values = array![3, 4].span();

let values = dojo::model::ModelEntity::<FooEntity>::values(@mvalues);
assert!(expected_values == values);
}

#[test]
fn test_from_values() {
let values = array![3, 4].span();

let model_entity = dojo::model::ModelEntity::<FooEntity>::from_values(1, values);
assert!(model_entity.__id == 1 && model_entity.v1 == 3 && model_entity.v2 == 4);
}

#[test]
#[should_panic(expected: "ModelEntity `FooEntity`: deserialization failed.")]
fn test_from_values_bad_data() {
let values = array![3].span();
let _ = dojo::model::ModelEntity::<FooEntity>::from_values(1, values);
}

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

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

let entity_id = foo.entity_id();
let mut entity = FooEntityTrait::get(world, entity_id);
assert!(entity.__id == entity_id && entity.v1 == entity.v1 && entity.v2 == entity.v2);

entity.v1 = 12;
entity.v2 = 18;

entity.update(world);

let read_values = FooEntityTrait::get(world, entity_id);
assert!(read_values.v1 == entity.v1 && read_values.v2 == entity.v2);
}

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

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

let entity_id = foo.entity_id();
let mut entity = FooEntityTrait::get(world, entity_id);
entity.delete(world);

let read_values = FooEntityTrait::get(world, entity_id);
assert!(read_values.v1 == 0 && read_values.v2 == 0);
}

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

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

let v1_raw_value: Span<felt252> = dojo::model::ModelEntity::<
FooEntity
>::get_member(world, foo.entity_id(), selector!("v1"));

assert!(v1_raw_value.len() == 1);
assert!(*v1_raw_value.at(0) == 3);

let entity = FooEntityTrait::get(world, foo.entity_id());
entity.set_member(world, selector!("v1"), array![42].span());

let entity = FooEntityTrait::get(world, foo.entity_id());
assert!(entity.v1 == 42);
}

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

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

let v1 = FooEntityTrait::get_v1(world, foo.entity_id());
assert!(foo.v1 == v1);

let entity = FooEntityTrait::get(world, foo.entity_id());
entity.set_v1(world, 42);

let v1 = FooEntityTrait::get_v1(world, foo.entity_id());
assert!(v1 == 42);
}

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

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

let read_entity = FooTrait::get(world, foo.k1, foo.k2);

assert!(
foo.k1 == read_entity.k1
&& foo.k2 == read_entity.k2
&& foo.v1 == read_entity.v1
&& foo.v2 == read_entity.v2
);
}

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

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

let read_entity = FooTrait::get(world, foo.k1, foo.k2);
assert!(
read_entity.k1 == foo.k1
&& read_entity.k2 == foo.k2
&& read_entity.v1 == 0
&& read_entity.v2 == 0
);
}

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

let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
let keys = array![foo.k1.into(), foo.k2.into()].span();
foo.set(world);

let v1_raw_value = dojo::model::Model::<Foo>::get_member(world, keys, selector!("v1"));

assert!(v1_raw_value.len() == 1);
assert!(*v1_raw_value.at(0) == 3);

foo.set_member(world, selector!("v1"), array![42].span());
let foo = FooTrait::get(world, foo.k1, foo.k2);
assert!(foo.v1 == 42);
}

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

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

let v1 = FooTrait::get_v1(world, foo.k1, foo.k2);
assert!(v1 == 3);

foo.set_v1(world, 42);

let v1 = FooTrait::get_v1(world, foo.k1, foo.k2);
assert!(v1 == 42);
}

65 changes: 56 additions & 9 deletions crates/dojo-core/src/resource_metadata.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
//! Manually expand to ensure that dojo-core
//! does not depend on dojo plugin to be built.
//!
use dojo::world::{IWorldDispatcherTrait};
use dojo::world::{IWorldDispatcherTrait, ModelIndex};
use dojo::model::Model;
use dojo::utils;

fn initial_address() -> starknet::ContractAddress {
starknet::contract_address_const::<0>()
Expand All @@ -24,12 +25,8 @@ struct ResourceMetadata {
}

impl ResourceMetadataModel of dojo::model::Model<ResourceMetadata> {
fn entity(
world: dojo::world::IWorldDispatcher,
keys: Span<felt252>,
layout: dojo::database::introspect::Layout
) -> ResourceMetadata {
let values = world.entity(Self::selector(), keys, layout);
fn get(world: dojo::world::IWorldDispatcher, keys: Span<felt252>) -> ResourceMetadata {
let values = world.entity(Self::selector(), ModelIndex::Keys(keys), Self::layout());
let mut serialized = core::array::ArrayTrait::new();
core::array::serialize_array_helper(keys, ref serialized);
core::array::serialize_array_helper(values, ref serialized);
Expand All @@ -45,6 +42,51 @@ impl ResourceMetadataModel of dojo::model::Model<ResourceMetadata> {
core::option::OptionTrait::<ResourceMetadata>::unwrap(entity)
}

fn set(self: @ResourceMetadata, world: dojo::world::IWorldDispatcher,) {
dojo::world::IWorldDispatcherTrait::set_entity(
world, Self::selector(), ModelIndex::Keys(self.keys()), self.values(), Self::layout()
);
}

fn delete(self: @ResourceMetadata, world: dojo::world::IWorldDispatcher,) {
world.delete_entity(Self::selector(), ModelIndex::Keys(self.keys()), Self::layout());
}

fn get_member(
world: dojo::world::IWorldDispatcher, keys: Span<felt252>, member_id: felt252
) -> Span<felt252> {
match utils::find_model_field_layout(Self::layout(), member_id) {
Option::Some(field_layout) => {
let entity_id = utils::entity_id_from_keys(keys);
world
.entity(
Self::selector(), ModelIndex::MemberId((entity_id, member_id)), field_layout
)
},
Option::None => panic_with_felt252('bad member id')
}
}

fn set_member(
self: @ResourceMetadata,
world: dojo::world::IWorldDispatcher,
member_id: felt252,
values: Span<felt252>
) {
match utils::find_model_field_layout(Self::layout(), member_id) {
Option::Some(field_layout) => {
world
.set_entity(
Self::selector(),
ModelIndex::MemberId((self.entity_id(), member_id)),
values,
field_layout
)
},
Option::None => panic_with_felt252('bad member id')
}
}

#[inline(always)]
fn name() -> ByteArray {
"ResourceMetadata"
Expand Down Expand Up @@ -74,11 +116,16 @@ impl ResourceMetadataModel of dojo::model::Model<ResourceMetadata> {
}

fn name_hash() -> felt252 {
dojo::utils::hash(@Self::name())
utils::hash(@Self::name())
}

fn namespace_hash() -> felt252 {
dojo::utils::hash(@Self::namespace())
utils::hash(@Self::namespace())
}

#[inline(always)]
fn entity_id(self: @ResourceMetadata) -> felt252 {
poseidon::poseidon_hash_span(self.keys())
}

#[inline(always)]
Expand Down
Loading
Loading