diff --git a/Scarb.lock b/Scarb.lock index e074b54..d878682 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -9,6 +9,14 @@ dependencies = [ "dojo_cairo_test", ] +[[package]] +name = "arcade_slot" +version = "0.0.0" +dependencies = [ + "dojo", + "dojo_cairo_test", +] + [[package]] name = "arcade_trophy" version = "0.0.0" diff --git a/Scarb.toml b/Scarb.toml index 5c79e6f..301415a 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -1,5 +1,5 @@ [workspace] -members = ["packages/trophy", "packages/registry"] +members = ["packages/trophy", "packages/registry", "packages/slot"] description = "Dojo achievement library" homepage = "https://github.com/cartridge-gg/arcade/" cairo-version = "2.8.4" diff --git a/packages/slot/README.md b/packages/slot/README.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/slot/Scarb.toml b/packages/slot/Scarb.toml new file mode 100644 index 0000000..a1b34c0 --- /dev/null +++ b/packages/slot/Scarb.toml @@ -0,0 +1,9 @@ +[package] +name = "arcade_slot" +version.workspace = true + +[dependencies] +dojo.workspace = true + +[dev-dependencies] +dojo_cairo_test.workspace = true diff --git a/packages/slot/src/constants.cairo b/packages/slot/src/constants.cairo new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/slot/src/constants.cairo @@ -0,0 +1 @@ + diff --git a/packages/slot/src/helpers/json.cairo b/packages/slot/src/helpers/json.cairo new file mode 100644 index 0000000..7a3c218 --- /dev/null +++ b/packages/slot/src/helpers/json.cairo @@ -0,0 +1,172 @@ +//! JSON helper functions + +pub trait JsonableTrait { + fn jsonify(self: T) -> ByteArray; +} + +pub impl Jsonable, +core::fmt::Display> of JsonableTrait { + fn jsonify(self: T) -> ByteArray { + format!("{}", self) + } +} + +#[generate_trait] +pub impl JsonableSimple of JsonableSimpleTrait { + fn jsonify(name: ByteArray, value: ByteArray) -> ByteArray { + format!("\"{}\":{}", name, value) + } +} + +#[generate_trait] +pub impl JsonableString of JsonableStringTrait { + fn jsonify(name: ByteArray, value: ByteArray) -> ByteArray { + format!("\"{}\":\"{}\"", name, value) + } +} + +#[generate_trait] +pub impl JsonableArray, +Drop> of JsonableArrayTrait { + fn jsonify(name: ByteArray, mut value: Array) -> ByteArray { + let mut string = "["; + let mut index: u32 = 0; + while let Option::Some(item) = value.pop_front() { + if index > 0 { + string += ","; + } + string += item.jsonify(); + index += 1; + }; + JsonableSimple::jsonify(name, string + "]") + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Jsonable, JsonableSimple, JsonableString, JsonableArray, JsonableTrait}; + + #[derive(Drop)] + struct BooleanObject { + value: bool, + } + + #[derive(Drop)] + struct IntegerObject { + value: u8, + } + + #[derive(Drop)] + struct FeltObject { + value: felt252, + } + + #[derive(Drop)] + struct ByteArrayObject { + value: ByteArray, + } + + #[derive(Drop)] + struct Complex { + boolean: bool, + integer: u8, + felt: felt252, + byte_array: ByteArray, + array: Array, + object_array: Array, + object: IntegerObject, + } + + pub impl IntegerObjectJsonable of JsonableTrait { + fn jsonify(self: IntegerObject) -> ByteArray { + let mut string = "{"; + string += JsonableSimple::jsonify("value", format!("{}", self.value)); + string + "}" + } + } + + pub impl BooleanObjectJsonable of JsonableTrait { + fn jsonify(self: BooleanObject) -> ByteArray { + let mut string = "{"; + string += JsonableSimple::jsonify("value", format!("{}", self.value)); + string + "}" + } + } + + pub impl FeltObjectJsonable of JsonableTrait { + fn jsonify(self: FeltObject) -> ByteArray { + let mut string = "{"; + string += JsonableSimple::jsonify("value", format!("{}", self.value)); + string + "}" + } + } + + pub impl ByteArrayObjectJsonable of JsonableTrait { + fn jsonify(self: ByteArrayObject) -> ByteArray { + let mut string = "{"; + string += JsonableString::jsonify("value", format!("{}", self.value)); + string + "}" + } + } + + pub impl ComplexJsonable of JsonableTrait { + fn jsonify(self: Complex) -> ByteArray { + let mut string = "{"; + string += JsonableSimple::jsonify("boolean", format!("{}", self.boolean)); + string += "," + JsonableSimple::jsonify("integer", format!("{}", self.integer)); + string += "," + JsonableSimple::jsonify("felt", format!("{}", self.felt)); + string += "," + JsonableString::jsonify("byte_array", format!("{}", self.byte_array)); + string += "," + JsonableArray::jsonify("array", self.array); + string += "," + JsonableArray::jsonify("object_array", self.object_array); + string += "," + JsonableSimple::jsonify("object", self.object.jsonify()); + string + "}" + } + } + + #[test] + fn test_jsonify_integer_object() { + let integer_object = IntegerObject { value: 1 }; + let json = integer_object.jsonify(); + assert_eq!(json, "{\"value\":1}"); + } + + #[test] + fn test_jsonify_boolean_object() { + let boolean_object = BooleanObject { value: true }; + let json = boolean_object.jsonify(); + assert_eq!(json, "{\"value\":true}"); + } + + #[test] + fn test_jsonify_felt_object() { + let felt_object = FeltObject { value: '1' }; + let json = felt_object.jsonify(); + assert_eq!(json, "{\"value\":49}"); + } + + #[test] + fn test_jsonify_byte_array_object() { + let byte_array_object = ByteArrayObject { value: "test" }; + let json = byte_array_object.jsonify(); + assert_eq!(json, "{\"value\":\"test\"}"); + } + + #[test] + fn test_jsonify_complex() { + let complex = Complex { + boolean: true, + integer: 1, + felt: '1', + byte_array: "test", + array: array![1, 2, 3], + object_array: array![IntegerObject { value: 1 }, IntegerObject { value: 2 }], + object: IntegerObject { value: 1 }, + }; + let json = complex.jsonify(); + assert_eq!( + json, + "{\"boolean\":true,\"integer\":1,\"felt\":49,\"byte_array\":\"test\",\"array\":[1,2,3],\"object_array\":[{\"value\":1},{\"value\":2}],\"object\":{\"value\":1}}" + ); + } +} + diff --git a/packages/slot/src/lib.cairo b/packages/slot/src/lib.cairo new file mode 100644 index 0000000..98b8685 --- /dev/null +++ b/packages/slot/src/lib.cairo @@ -0,0 +1,16 @@ +mod constants; +mod store; + +mod helpers { + mod json; +} + +mod types { + mod tier; + mod status; +} + +mod models { + mod index; + mod deployment; +} diff --git a/packages/slot/src/models/deployment.cairo b/packages/slot/src/models/deployment.cairo new file mode 100644 index 0000000..b3369e4 --- /dev/null +++ b/packages/slot/src/models/deployment.cairo @@ -0,0 +1,157 @@ +// Intenral imports + +use arcade_slot::models::index::Deployment; +use arcade_slot::types::status::Status; +use arcade_slot::types::tier::Tier; + +// Errors + +pub mod errors { + pub const DEPLOYMENT_ALREADY_EXISTS: felt252 = 'Deployment: already exists'; + pub const DEPLOYMENT_NOT_EXIST: felt252 = 'Deployment: does not exist'; + pub const DEPLOYMENT_INVALID_IDENTIFIER: felt252 = 'Deployment: invalid identifier'; + pub const DEPLOYMENT_INVALID_PROJECT: felt252 = 'Deployment: invalid project'; + pub const DEPLOYMENT_INVALID_STATUS: felt252 = 'Deployment: invalid status'; + pub const DEPLOYMENT_INVALID_SERVICE_ID: felt252 = 'Deployment: invalid service id'; + pub const DEPLOYMENT_INVALID_TIER: felt252 = 'Deployment: invalid tier'; + pub const DEPLOYMENT_INVALID_REGIONS: felt252 = 'Deployment: invalid regions'; +} + +#[generate_trait] +impl DeploymentImpl of DeploymentTrait { + #[inline] + fn new( + id: felt252, + project: felt252, + status: Status, + branch: Option, + service_id: felt252, + tier: Tier, + regions: felt252, + auto_upgrade: bool, + config: ByteArray, + ) -> Deployment { + // [Check] Inputs + DeploymentAssert::assert_valid_identifier(id); + DeploymentAssert::assert_valid_project(project); + DeploymentAssert::assert_valid_status(status); + DeploymentAssert::assert_valid_service_id(service_id); + DeploymentAssert::assert_valid_tier(tier); + DeploymentAssert::assert_valid_regions(regions); + // [Return] Deployment + let time = starknet::get_block_timestamp(); + Deployment { + id: id, + project: project, + status: status.into(), + branch: branch, + service_id: service_id, + tier: tier.into(), + regions: regions, + auto_upgrade: auto_upgrade, + config: config, + created_at: time, + updated_at: time, + spin_down_at: Option::None, + spin_up_at: Option::None, + } + } +} + +#[generate_trait] +impl DeploymentAssert of AssertTrait { + #[inline] + fn assert_does_not_exist(self: Deployment) { + assert(self.project == 0, errors::DEPLOYMENT_ALREADY_EXISTS); + } + + #[inline] + fn assert_does_exist(self: Deployment) { + assert(self.project != 0, errors::DEPLOYMENT_NOT_EXIST); + } + + #[inline] + fn assert_valid_identifier(identifier: felt252) { + assert(identifier != 0, errors::DEPLOYMENT_INVALID_IDENTIFIER); + } + + #[inline] + fn assert_valid_project(project: felt252) { + assert(project != 0, errors::DEPLOYMENT_INVALID_PROJECT); + } + + #[inline] + fn assert_valid_status(status: Status) { + assert(status != Status::None, errors::DEPLOYMENT_INVALID_STATUS); + } + + #[inline] + fn assert_valid_service_id(service_id: felt252) { + assert(service_id != 0, errors::DEPLOYMENT_INVALID_SERVICE_ID); + } + + #[inline] + fn assert_valid_tier(tier: Tier) { + assert(tier != Tier::None, errors::DEPLOYMENT_INVALID_TIER); + } + + #[inline] + fn assert_valid_regions(regions: felt252) { + assert(regions != 0, errors::DEPLOYMENT_INVALID_REGIONS); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Deployment, DeploymentTrait, DeploymentAssert, Status, Tier}; + + // Constants + + const IDENTIFIER: felt252 = 'ID'; + const PROJECT: felt252 = 'PROJECT'; + const STATUS: Status = Status::Active; + const BRANCH: Option = Option::None; + const SERVICE_ID: felt252 = 'SERVICE_ID'; + const TIER: Tier = Tier::Basic; + const REGIONS: felt252 = 'REGIONS'; + const AUTO_UPGRADE: bool = true; + + #[test] + fn test_deployment_new() { + let deployment = DeploymentTrait::new( + IDENTIFIER, PROJECT, STATUS, BRANCH, SERVICE_ID, TIER, REGIONS, AUTO_UPGRADE, "" + ); + assert_eq!(deployment.id, IDENTIFIER); + assert_eq!(deployment.project, PROJECT); + assert_eq!(deployment.status, STATUS.into()); + assert_eq!(deployment.branch, BRANCH); + assert_eq!(deployment.service_id, SERVICE_ID); + assert_eq!(deployment.tier, TIER.into()); + assert_eq!(deployment.regions, REGIONS); + assert_eq!(deployment.auto_upgrade, AUTO_UPGRADE); + assert_eq!(deployment.config, ""); + assert_eq!(deployment.created_at, starknet::get_block_timestamp()); + assert_eq!(deployment.updated_at, starknet::get_block_timestamp()); + assert_eq!(deployment.spin_down_at, Option::None); + assert_eq!(deployment.spin_up_at, Option::None); + } + + #[test] + fn test_deployment_assert_does_exist() { + let deployment = DeploymentTrait::new( + IDENTIFIER, PROJECT, STATUS, BRANCH, SERVICE_ID, TIER, REGIONS, AUTO_UPGRADE, "" + ); + deployment.assert_does_exist(); + } + + #[test] + #[should_panic(expected: 'Deployment: already exists')] + fn test_deployment_revert_already_exists() { + let deployment = DeploymentTrait::new( + IDENTIFIER, PROJECT, STATUS, BRANCH, SERVICE_ID, TIER, REGIONS, AUTO_UPGRADE, "" + ); + deployment.assert_does_not_exist(); + } +} diff --git a/packages/slot/src/models/index.cairo b/packages/slot/src/models/index.cairo new file mode 100644 index 0000000..6fa5b26 --- /dev/null +++ b/packages/slot/src/models/index.cairo @@ -0,0 +1,116 @@ +/// Models + +// package schema + +// import ( +// "encoding/json" +// "time" + +// "entgo.io/contrib/entgql" +// "entgo.io/ent" +// "entgo.io/ent/schema" +// "entgo.io/ent/schema/edge" +// "entgo.io/ent/schema/field" +// "entgo.io/ent/schema/index" +// "github.com/lucsky/cuid" +// ) + +// type Resources struct { +// Memory float64 `json:"memory,omitempty"` +// CPU float64 `json:"cpu,omitempty"` +// } + +// type MachineSpecs struct { +// Requests *Resources `json:"requests,omitempty"` +// Limits *Resources `json:"limits,omitempty"` +// Storage int `json:"storage,omitempty"` +// } + +// // Deployment holds the schema definition for the Deployment entity. +// type Deployment struct { +// ent.Schema +// } + +// // Fields of the Deployment. +// func (Deployment) Fields() []ent.Field { +// return []ent.Field{ +// field.String("id"). +// Unique(). +// Immutable(). +// DefaultFunc(cuid.New), +// field.String("project"), +// field.Enum("status"). +// Values("active", "disabled"). +// Default("active"), +// field.String("branch").Default("main").Optional(), +// field.String("service_id"), +// field.Enum("tier").Values( +// "basic", +// "common", +// "uncommon", +// "rare", +// "epic", +// "legendary", +// "insane", +// ), +// field.Strings("regions"), +// field.Bool("auto_upgrade").Default(false), +// field.JSON("config", json.RawMessage{}). +// Annotations(entgql.Skip()). +// Optional(), +// field.Time("created_at"). +// Default(time.Now). +// Annotations( +// entgql.OrderField("CREATED_AT"), +// ), +// field.Time("updated_at"). +// Default(time.Now). +// UpdateDefault(time.Now), +// field.Time("spin_down_at").Optional(), +// field.Time("spin_up_at").Optional(), +// } +// } + +// // Edges of the Deployment. +// func (Deployment) Edges() []ent.Edge { +// return []ent.Edge{ +// edge.From("teams", Team.Type). +// Ref("deployments"). +// Annotations(entgql.RelayConnection()), +// edge.From("service", Service.Type).Ref("deployments").Field("service_id").Unique().Required(), +// edge.To("events", DeploymentLog.Type), +// } +// } + +// func (Deployment) Indexes() []ent.Index { +// return []ent.Index{ +// index.Fields("project", "service_id").Unique(), +// } +// } + +// // Annotations returns Deployment annotations. +// func (Deployment) Annotations() []schema.Annotation { +// return []schema.Annotation{ +// entgql.RelayConnection(), +// } +// } + +#[derive(Clone, Drop, Serde)] +#[dojo::model] +pub struct Deployment { + #[key] + id: felt252, + #[key] + project: felt252, + status: u8, + branch: Option, + service_id: felt252, + tier: u8, + regions: felt252, + auto_upgrade: bool, + config: ByteArray, + created_at: u64, + updated_at: u64, + spin_down_at: Option, + spin_up_at: Option, +} diff --git a/packages/slot/src/store.cairo b/packages/slot/src/store.cairo new file mode 100644 index 0000000..023d2f2 --- /dev/null +++ b/packages/slot/src/store.cairo @@ -0,0 +1,40 @@ +//! Store struct and component management methods. + +// Starknet imports + +use starknet::SyscallResultTrait; + +// Dojo imports + +use dojo::world::WorldStorage; +use dojo::model::ModelStorage; +// Models imports + +use arcade_slot::models::deployment::Deployment; + +// Structs + +#[derive(Copy, Drop)] +struct Store { + world: WorldStorage, +} + +// Implementations + +#[generate_trait] +impl StoreImpl of StoreTrait { + #[inline] + fn new(world: WorldStorage) -> Store { + Store { world: world } + } + + #[inline] + fn get_deployment(self: Store, deployment_id: u32) -> Deployment { + self.world.read_model(deployment_id) + } + + #[inline] + fn set_deployment(ref self: Store, deployment: Deployment) { + self.world.write_model(@deployment); + } +} diff --git a/packages/slot/src/types/status.cairo b/packages/slot/src/types/status.cairo new file mode 100644 index 0000000..10bc447 --- /dev/null +++ b/packages/slot/src/types/status.cairo @@ -0,0 +1,31 @@ +#[derive(Copy, Drop, PartialEq)] +pub enum Status { + None, + Active, + Disabled, +} + +// Implementations + +impl IntoStatusU8 of core::Into { + #[inline] + fn into(self: Status) -> u8 { + match self { + Status::None => 0, + Status::Active => 1, + Status::Disabled => 2, + } + } +} + +impl IntoU8Status of core::Into { + #[inline] + fn into(self: u8) -> Status { + match self { + 0 => Status::None, + 1 => Status::Active, + 2 => Status::Disabled, + _ => Status::None, + } + } +} diff --git a/packages/slot/src/types/tier.cairo b/packages/slot/src/types/tier.cairo new file mode 100644 index 0000000..6233602 --- /dev/null +++ b/packages/slot/src/types/tier.cairo @@ -0,0 +1,46 @@ +#[derive(Copy, Drop, PartialEq)] +pub enum Tier { + None, + Basic, + Common, + Uncommon, + Rare, + Epic, + Legendary, + Insane, +} + +// Implementations + +impl IntoTierU8 of core::Into { + #[inline] + fn into(self: Tier) -> u8 { + match self { + Tier::None => 0, + Tier::Basic => 1, + Tier::Common => 2, + Tier::Uncommon => 3, + Tier::Rare => 4, + Tier::Epic => 5, + Tier::Legendary => 6, + Tier::Insane => 7, + } + } +} + +impl IntoU8Tier of core::Into { + #[inline] + fn into(self: u8) -> Tier { + match self { + 0 => Tier::None, + 1 => Tier::Basic, + 2 => Tier::Common, + 3 => Tier::Uncommon, + 4 => Tier::Rare, + 5 => Tier::Epic, + 6 => Tier::Legendary, + 7 => Tier::Insane, + _ => Tier::None, + } + } +}