From 9ff1cf1bace194af118246b058b37efccb1313c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Mon, 3 Nov 2025 16:00:59 +0100 Subject: [PATCH] Initial Commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- .envrc | 1 + .gitignore | 4 + Cargo.lock | 14 +++ Cargo.toml | 11 ++ crates/tytix-core/Cargo.toml | 17 +++ crates/tytix-core/src/lib.rs | 235 +++++++++++++++++++++++++++++++++++ crates/tytix/Cargo.toml | 12 ++ crates/tytix/src/lib.rs | 123 ++++++++++++++++++ flake.lock | 97 +++++++++++++++ flake.nix | 96 ++++++++++++++ rust-toolchain.toml | 2 + rustfmt.toml | 3 + 12 files changed, 615 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/tytix-core/Cargo.toml create mode 100644 crates/tytix-core/src/lib.rs create mode 100644 crates/tytix/Cargo.toml create mode 100644 crates/tytix/src/lib.rs create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 rust-toolchain.toml create mode 100644 rustfmt.toml diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc4dcbe --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.direnv + +/target +/result* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b6155e1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "tytix" +version = "0.1.0" +dependencies = [ + "tytix-core", +] + +[[package]] +name = "tytix-core" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a1e058a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] + +members = ["crates/*"] + +resolver = "3" + +[workspace.package] +edition = "2024" +version = "0.1.0" +license = "EUPL-1.2" +authors = ["Marcel Müller "] diff --git a/crates/tytix-core/Cargo.toml b/crates/tytix-core/Cargo.toml new file mode 100644 index 0000000..071be85 --- /dev/null +++ b/crates/tytix-core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tytix-core" +version.workspace = true +edition.workspace = true +authors.workspace = true + +description = "A clear and simple to use actor framework" +repository = "https://github.com/TheNeikos/tytix" +license = "EUPL-1.2" +keywords = [] +categories = [] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +[dev-dependencies] diff --git a/crates/tytix-core/src/lib.rs b/crates/tytix-core/src/lib.rs new file mode 100644 index 0000000..2f9d629 --- /dev/null +++ b/crates/tytix-core/src/lib.rs @@ -0,0 +1,235 @@ +#![feature(const_cmp)] +#![feature(const_trait_impl)] + +use std::any::Any; +use std::any::TypeId; + +pub trait Message: Send + Any { + type Reply: Send + Any; +} + +pub trait MessageIdentifier { + const IDENT: BundleChain; +} + +impl MessageIdentifier for M { + const IDENT: BundleChain = BundleChain::of::(); +} + +pub struct InternalMessage(Box); + +impl InternalMessage { + pub fn new(message: impl Message) -> InternalMessage { + InternalMessage(Box::new(message)) + } + + pub fn into_inner(self) -> Result { + self.0.downcast().map(|v| *v).map_err(InternalMessage) + } +} + +pub trait Address +where + MB: MessageBundle, +{ + fn send(&mut self, message: M); +} + +pub trait InternalMessageHandler { + type HandledMessages: MessageBundle; + fn handle_message(&mut self, msg: InternalMessage); +} + +impl Address for IMH +where + MB: MessageBundle, + IMH: InternalMessageHandler, +{ + fn send(&mut self, message: M) { + const { + let true = >::IS_CONTAINED else { + panic!("Message is not contained in MessageBundle",); + }; + } + + self.handle_message(InternalMessage(Box::new(message))); + } +} + +pub trait MessageReceiver {} + +pub trait MessageBundle { + const IDS: BundleChain; +} + +impl MessageBundle for () { + const IDS: BundleChain = BundleChain { + next: None, + op: BundleOp::Remove(TypeId::of::<()>()), + }; +} + +impl MessageBundle for (M,) { + const IDS: BundleChain = BundleChain::of::(); +} + +#[derive(Debug, Clone, Copy)] +pub struct BundleChain { + op: BundleOp, + next: Option<&'static BundleChain>, +} + +#[derive(Debug, Clone, Copy)] +pub enum BundleOp { + Add(TypeId), + Remove(TypeId), +} + +impl BundleChain { + pub const fn of() -> BundleChain { + BundleChain { + op: BundleOp::Add(TypeId::of::()), + next: None, + } + } + + pub const fn with(&'static self) -> BundleChain { + add_to_chain(self, TypeId::of::()) + } + + pub const fn without(&'static self) -> BundleChain { + remove_from_chain(self, TypeId::of::()) + } +} + +impl MessageBundle for (A, B) +where + A: Message, + B: Message, +{ + const IDS: BundleChain = BundleChain::of::().with::(); +} + +impl MessageBundle for (A, B, C, D) +where + A: Message, + B: Message, + C: Message, + D: Message, +{ + const IDS: BundleChain = BundleChain::of::().with::().with::().with::(); +} + +pub trait IsContainedInBundle { + const IS_CONTAINED: bool; +} + +impl IsContainedInBundle for M +where + M: Message, + MB: MessageBundle, +{ + const IS_CONTAINED: bool = check_is_contained(&MB::IDS, TypeId::of::()); +} + +const fn check_is_contained(ids: &'static BundleChain, id: TypeId) -> bool { + match ids.op { + BundleOp::Add(added_id) => { + if check_type_id_equal(added_id, id) { + return true; + } + } + BundleOp::Remove(removed_id) => { + if check_type_id_equal(removed_id, id) { + return false; + } + } + } + + if let Some(next) = ids.next { + check_is_contained(next, id) + } else { + false + } +} + +const fn check_type_id_equal(left: TypeId, right: TypeId) -> bool { + left == right +} + +const fn add_to_chain(prev: &'static BundleChain, to_add: TypeId) -> BundleChain { + BundleChain { + op: BundleOp::Add(to_add), + next: Some(prev), + } +} + +const fn remove_from_chain(prev: &'static BundleChain, to_remove: TypeId) -> BundleChain { + BundleChain { + op: BundleOp::Remove(to_remove), + next: Some(prev), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct Foo; + + struct Bar; + + impl Message for Foo { + type Reply = (); + } + + impl Message for Bar { + type Reply = (); + } + + struct Zap; + + impl Message for Zap { + type Reply = (); + } + + #[test] + fn check_checker() { + assert!(check_type_id_equal( + TypeId::of::(), + TypeId::of::() + )); + + const BUNDLE: BundleChain = BundleChain::of::().with::(); + + assert!(check_is_contained(&BUNDLE, TypeId::of::())); + assert!(!check_is_contained(&BUNDLE, TypeId::of::())); + + const WITHOUT_BUNDLE: BundleChain = BundleChain::of::().with::().without::(); + + assert!(!check_is_contained(&WITHOUT_BUNDLE, TypeId::of::())); + + const WITHOUT_WITH_BUNDLE: BundleChain = + BundleChain::of::().without::().with::(); + + assert!(check_is_contained( + &WITHOUT_WITH_BUNDLE, + TypeId::of::() + )); + } + + #[test] + fn check_sending_messages() { + struct Sender; + + impl InternalMessageHandler for Sender { + type HandledMessages = (Foo, Bar); + + fn handle_message(&mut self, _msg: InternalMessage) {} + } + + let mut s = Sender; + + s.send(Foo); + } +} diff --git a/crates/tytix/Cargo.toml b/crates/tytix/Cargo.toml new file mode 100644 index 0000000..e7e8af6 --- /dev/null +++ b/crates/tytix/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tytix" +version.workspace = true +edition.workspace = true +authors.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tytix-core = { path = "../tytix-core/" } + +[dev-dependencies] diff --git a/crates/tytix/src/lib.rs b/crates/tytix/src/lib.rs new file mode 100644 index 0000000..5b3512b --- /dev/null +++ b/crates/tytix/src/lib.rs @@ -0,0 +1,123 @@ +use std::marker::PhantomData; + +use tytix_core::Address; +use tytix_core::InternalMessageHandler; +use tytix_core::IsContainedInBundle; +use tytix_core::Message; +use tytix_core::MessageBundle; + +pub trait AddressExt { + fn map_before(self, f: F) -> MappedBeforeAddress + where + MB: MessageBundle, + F: Fn(M) -> R + 'static, + M: Message, + R: Message + IsContainedInBundle, + Self: Sized; +} + +impl> AddressExt for A { + fn map_before(self, f: F) -> MappedBeforeAddress + where + MB: MessageBundle, + F: Fn(M) -> R + 'static, + M: Message, + R: Message + IsContainedInBundle, + Self: Sized, + { + const { + let true = >::IS_CONTAINED else { + panic!("Message is not contained in MessageBundle",); + }; + } + + MappedBeforeAddress { + address: self, + func: f, + _pd: PhantomData, + } + } +} + +pub struct MappedBeforeAddress { + address: A, + func: F, + _pd: PhantomData R>, +} + +pub struct MappedHandledMessages { + _pd: PhantomData R>, +} + +impl MessageBundle for MappedHandledMessages +where + MB: MessageBundle, + M: Message, + R: Message + IsContainedInBundle, +{ + const IDS: tytix_core::BundleChain = MB::IDS.without::().with::(); +} + +impl InternalMessageHandler for MappedBeforeAddress +where + A: InternalMessageHandler, + M: Message, + R: Message + IsContainedInBundle, + F: FnMut(M) -> R, +{ + type HandledMessages = MappedHandledMessages; + + fn handle_message(&mut self, msg: tytix_core::InternalMessage) { + match msg.into_inner::() { + Ok(removed_message) => { + let new_message = (self.func)(removed_message); + + self.address.send(new_message); + } + Err(other_message) => self.address.handle_message(other_message), + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::OnceLock; + + use super::*; + + struct Foo; + + impl Message for Foo { + type Reply = (); + } + + struct Bar; + + impl Message for Bar { + type Reply = (); + } + + struct SimpleAddress; + + impl InternalMessageHandler for SimpleAddress { + type HandledMessages = (Foo,); + + fn handle_message(&mut self, msg: tytix_core::InternalMessage) { + drop(msg); + } + } + + #[test] + fn check_mapping() { + static MSG: OnceLock = OnceLock::new(); + + let mut sa = SimpleAddress.map_before(|_b: Bar| { + let _ = MSG.set(true); + Foo + }); + + sa.send(Bar); + + MSG.get().expect("The message was mapped!"); + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..607391e --- /dev/null +++ b/flake.lock @@ -0,0 +1,97 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1736101677, + "narHash": "sha256-iKOPq86AOWCohuzxwFy/MtC8PcSVGnrxBOvxpjpzrAY=", + "owner": "ipetkov", + "repo": "crane", + "rev": "61ba163d85e5adeddc7b3a69bb174034965965b2", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1736200483, + "narHash": "sha256-JO+lFN2HsCwSLMUWXHeOad6QUxOuwe9UOAF/iSl1J4I=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3f0a8ac25fb674611b98089ca3a5dd6480175751", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-24.11", + "type": "indirect" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762051177, + "narHash": "sha256-pESNTx/m3WnrYx+OujBtDP5Bj0/mAyHa4MgEwzkgkLE=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "08c33e87c4829bbdd42b5af247cf7a19e126369f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2121466 --- /dev/null +++ b/flake.nix @@ -0,0 +1,96 @@ +{ + description = "The Theater Rust library"; + inputs = { + nixpkgs.url = "nixpkgs/nixos-24.11"; + flake-utils = { + url = "github:numtide/flake-utils"; + }; + crane = { + url = "github:ipetkov/crane"; + }; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs = { + nixpkgs.follows = "nixpkgs"; + }; + }; + }; + + outputs = { nixpkgs, crane, flake-utils, rust-overlay, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + + rustTarget = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; + unstableRustTarget = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default.override { + extensions = [ "rust-src" "miri" "rustfmt" ]; + }); + craneLib = (crane.mkLib pkgs).overrideToolchain rustTarget; + unstableCraneLib = (crane.mkLib pkgs).overrideToolchain unstableRustTarget; + + tomlInfo = craneLib.crateNameFromCargoToml { cargoToml = ./Cargo.toml; }; + inherit (tomlInfo) version; + src = ./.; + + rustfmt' = pkgs.writeShellScriptBin "rustfmt" '' + exec "${unstableRustTarget}/bin/rustfmt" "$@" + ''; + + cargoArtifacts = craneLib.buildDepsOnly { + inherit src; + cargoExtraArgs = "--all-features --all"; + }; + + cloudmqtt = craneLib.buildPackage { + inherit cargoArtifacts src version; + cargoExtraArgs = "--all-features --all"; + }; + + in + rec { + checks = { + inherit cloudmqtt; + + cloudmqtt-clippy = craneLib.cargoClippy { + inherit cargoArtifacts src; + cargoExtraArgs = "--all --all-features"; + cargoClippyExtraArgs = "-- --deny warnings"; + }; + + cloudmqtt-fmt = unstableCraneLib.cargoFmt { + inherit src; + }; + }; + + packages.cloudmqtt = cloudmqtt; + packages.default = packages.cloudmqtt; + + apps.cloudmqtt = flake-utils.lib.mkApp { + name = "cloudmqtt"; + drv = cloudmqtt; + }; + apps.default = apps.cloudmqtt; + + devShells.default = devShells.cloudmqtt; + devShells.cloudmqtt = pkgs.mkShell { + buildInputs = [ ]; + + nativeBuildInputs = [ + rustfmt' + unstableRustTarget + + pkgs.cargo-msrv + pkgs.cargo-deny + pkgs.cargo-expand + pkgs.cargo-bloat + pkgs.cargo-fuzz + + pkgs.gitlint + ]; + }; + } + ); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..d72668b --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.91.0" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..84d1a61 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +imports_granularity = "Item" +reorder_imports = true +group_imports = "StdExternalCrate"