Skip to content

Commit

Permalink
feat: add get_[field_name] and set_[field_name] to models
Browse files Browse the repository at this point in the history
  • Loading branch information
remybar committed Jul 12, 2024
1 parent d89c1e8 commit 5bf5b1f
Show file tree
Hide file tree
Showing 40 changed files with 6,428 additions and 2,359 deletions.
Binary file added ._target
Binary file not shown.
2 changes: 1 addition & 1 deletion crates/dojo-core/src/base_test.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ mod invalid_model {
fn selector(self: @ContractState) -> felt252 {
// NOTE: Need to update this value if address changes
// Pre-computed address of a contract deployed through the world.
0x562c876783650ced854d9571abba1259cc28b5256fa0994f3d1c4cd2b6c4488
0x3f18f40f6d815f380d1ea8278289b58f076abf6e396d7a88eed5d152da8190b
}

fn namespace(self: @ContractState) -> ByteArray {
Expand Down
39 changes: 20 additions & 19 deletions crates/dojo-core/src/model.cairo
Original file line number Diff line number Diff line change
@@ -1,31 +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 ModelValues<T> {
trait ModelEntity<T> {
fn id(self: @T) -> felt252;
fn values(self: @T) -> Span<felt252>;
fn from_values(values: Span<felt252>) -> T;
fn get(world: IWorldDispatcher, id: felt252) -> T;
fn update(self: @T, world: IWorldDispatcher, id: felt252);
fn delete(self: @T, world: IWorldDispatcher, id: 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 set_entity(
world: IWorldDispatcher,
keys: Span<felt252>,
values: Span<felt252>,
layout: dojo::database::introspect::Layout
);

fn delete_entity(
world: IWorldDispatcher, keys: Span<felt252>, layout: dojo::database::introspect::Layout
);
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 Down
174 changes: 123 additions & 51 deletions crates/dojo-core/src/model_test.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -17,110 +17,182 @@ struct Foo {
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 = FooValues { v1: 3, v2: 4 };
let mvalues = FooEntity { __id: 1, v1: 3, v2: 4 };
let expected_values = array![3, 4].span();

let values = FooModelValues::values(@mvalues);
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_values = FooModelValues::from_values(values);
assert!(model_values.v1 == 3 && model_values.v2 == 4);
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: "ModelValues `FooValues`: deserialization failed.")]
#[should_panic(expected: "ModelEntity `FooEntity`: deserialization failed.")]
fn test_from_values_bad_data() {
let values = array![3].span();
let _ = FooModelValues::from_values(values);
let _ = dojo::model::ModelEntity::<FooEntity>::from_values(1, values);
}

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

let entity = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
let entity_id = entity.entity_id();
dojo::model::Model::<
Foo
>::set_entity(world, entity.keys(), entity.values(), entity.instance_layout());
let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
foo.set(world);

let mut entity_values = FooModelValues::get(world, entity_id);
assert!(entity.v1 == entity_values.v1 && entity.v2 == entity_values.v2);
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_values.v1 = 12;
entity_values.v2 = 18;
entity.v1 = 12;
entity.v2 = 18;

entity_values.update(world, entity_id);
entity.update(world);

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

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

let entity = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
let entity_id = entity.entity_id();
dojo::model::Model::<
Foo
>::set_entity(world, entity.keys(), entity.values(), entity.instance_layout());
let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
foo.set(world);

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

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

#[test]
fn test_entity_and_set_entity() {
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 entity = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
dojo::model::Model::<
Foo
>::set_entity(world, entity.keys(), entity.values(), entity.instance_layout());
let read_entity = dojo::model::Model::<
Foo
>::entity(world, entity.keys(), entity.instance_layout());
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!(
entity.k1 == read_entity.k1
&& entity.k2 == read_entity.k2
&& entity.v1 == read_entity.v1
&& entity.v2 == read_entity.v2
foo.k1 == read_entity.k1
&& foo.k2 == read_entity.k2
&& foo.v1 == read_entity.v1
&& foo.v2 == read_entity.v2
);
}

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

let entity = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
dojo::model::Model::<
Foo
>::set_entity(world, entity.keys(), entity.values(), entity.instance_layout());
dojo::model::Model::<Foo>::delete_entity(world, entity.keys(), entity.instance_layout());
let foo = Foo { k1: 1, k2: 2, v1: 3, v2: 4 };
foo.set(world);
foo.delete(world);

let read_entity = dojo::model::Model::<
Foo
>::entity(world, entity.keys(), entity.instance_layout());
let read_entity = FooTrait::get(world, foo.k1, foo.k2);
assert!(
read_entity.k1 == entity.k1
&& read_entity.k2 == entity.k2
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);
}

64 changes: 43 additions & 21 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,26 +42,51 @@ impl ResourceMetadataModel of dojo::model::Model<ResourceMetadata> {
core::option::OptionTrait::<ResourceMetadata>::unwrap(entity)
}

fn set_entity(
world: dojo::world::IWorldDispatcher,
keys: Span<felt252>,
values: Span<felt252>,
layout: dojo::database::introspect::Layout
) {
fn set(self: @ResourceMetadata, world: dojo::world::IWorldDispatcher,) {
dojo::world::IWorldDispatcherTrait::set_entity(
world, Self::selector(), keys, values, layout
world, Self::selector(), ModelIndex::Keys(self.keys()), self.values(), Self::layout()
);
}

fn delete_entity(
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,
keys: Span<felt252>,
layout: dojo::database::introspect::Layout
member_id: felt252,
values: Span<felt252>
) {
dojo::world::IWorldDispatcherTrait::delete_entity(world, Self::selector(), keys, layout);
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 @@ -94,11 +116,11 @@ 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)]
Expand Down
Loading

0 comments on commit 5bf5b1f

Please sign in to comment.