Initial Commit

Signed-off-by: Marcel Müller <neikos@neikos.email>
This commit is contained in:
Marcel Müller 2025-11-03 16:00:59 +01:00
commit 9ff1cf1bac
12 changed files with 615 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.direnv
/target
/result*

14
Cargo.lock generated Normal file
View file

@ -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"

11
Cargo.toml Normal file
View file

@ -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 <neikos@neikos.email>"]

View file

@ -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]

View file

@ -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<M: Message> MessageIdentifier for M {
const IDENT: BundleChain = BundleChain::of::<M>();
}
pub struct InternalMessage(Box<dyn std::any::Any>);
impl InternalMessage {
pub fn new(message: impl Message) -> InternalMessage {
InternalMessage(Box::new(message))
}
pub fn into_inner<M: Message>(self) -> Result<M, InternalMessage> {
self.0.downcast().map(|v| *v).map_err(InternalMessage)
}
}
pub trait Address<MB>
where
MB: MessageBundle,
{
fn send<M: Message>(&mut self, message: M);
}
pub trait InternalMessageHandler {
type HandledMessages: MessageBundle;
fn handle_message(&mut self, msg: InternalMessage);
}
impl<MB, IMH> Address<MB> for IMH
where
MB: MessageBundle,
IMH: InternalMessageHandler<HandledMessages = MB>,
{
fn send<M: Message>(&mut self, message: M) {
const {
let true = <M as IsContainedInBundle<MB>>::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<M: Message> MessageBundle for (M,) {
const IDS: BundleChain = BundleChain::of::<M>();
}
#[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<M: Message>() -> BundleChain {
BundleChain {
op: BundleOp::Add(TypeId::of::<M>()),
next: None,
}
}
pub const fn with<M: Message>(&'static self) -> BundleChain {
add_to_chain(self, TypeId::of::<M>())
}
pub const fn without<M: Message>(&'static self) -> BundleChain {
remove_from_chain(self, TypeId::of::<M>())
}
}
impl<A, B> MessageBundle for (A, B)
where
A: Message,
B: Message,
{
const IDS: BundleChain = BundleChain::of::<A>().with::<B>();
}
impl<A, B, C, D> MessageBundle for (A, B, C, D)
where
A: Message,
B: Message,
C: Message,
D: Message,
{
const IDS: BundleChain = BundleChain::of::<A>().with::<B>().with::<C>().with::<D>();
}
pub trait IsContainedInBundle<MB> {
const IS_CONTAINED: bool;
}
impl<M, MB> IsContainedInBundle<MB> for M
where
M: Message,
MB: MessageBundle,
{
const IS_CONTAINED: bool = check_is_contained(&MB::IDS, TypeId::of::<M>());
}
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::<bool>(),
TypeId::of::<bool>()
));
const BUNDLE: BundleChain = BundleChain::of::<Foo>().with::<Zap>();
assert!(check_is_contained(&BUNDLE, TypeId::of::<Foo>()));
assert!(!check_is_contained(&BUNDLE, TypeId::of::<Bar>()));
const WITHOUT_BUNDLE: BundleChain = BundleChain::of::<Foo>().with::<Zap>().without::<Zap>();
assert!(!check_is_contained(&WITHOUT_BUNDLE, TypeId::of::<Zap>()));
const WITHOUT_WITH_BUNDLE: BundleChain =
BundleChain::of::<Foo>().without::<Zap>().with::<Zap>();
assert!(check_is_contained(
&WITHOUT_WITH_BUNDLE,
TypeId::of::<Zap>()
));
}
#[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);
}
}

12
crates/tytix/Cargo.toml Normal file
View file

@ -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]

123
crates/tytix/src/lib.rs Normal file
View file

@ -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<MB> {
fn map_before<F, M, R>(self, f: F) -> MappedBeforeAddress<Self, F, M, R>
where
MB: MessageBundle,
F: Fn(M) -> R + 'static,
M: Message,
R: Message + IsContainedInBundle<MB>,
Self: Sized;
}
impl<MB: MessageBundle, A: Address<MB>> AddressExt<MB> for A {
fn map_before<F, M, R>(self, f: F) -> MappedBeforeAddress<Self, F, M, R>
where
MB: MessageBundle,
F: Fn(M) -> R + 'static,
M: Message,
R: Message + IsContainedInBundle<MB>,
Self: Sized,
{
const {
let true = <R as IsContainedInBundle<MB>>::IS_CONTAINED else {
panic!("Message is not contained in MessageBundle",);
};
}
MappedBeforeAddress {
address: self,
func: f,
_pd: PhantomData,
}
}
}
pub struct MappedBeforeAddress<A, F, M, R> {
address: A,
func: F,
_pd: PhantomData<fn(M) -> R>,
}
pub struct MappedHandledMessages<MB, M, R> {
_pd: PhantomData<fn(MB, M) -> R>,
}
impl<MB, M, R> MessageBundle for MappedHandledMessages<MB, M, R>
where
MB: MessageBundle,
M: Message,
R: Message + IsContainedInBundle<MB>,
{
const IDS: tytix_core::BundleChain = MB::IDS.without::<R>().with::<M>();
}
impl<A, F, M, R> InternalMessageHandler for MappedBeforeAddress<A, F, M, R>
where
A: InternalMessageHandler,
M: Message,
R: Message + IsContainedInBundle<A::HandledMessages>,
F: FnMut(M) -> R,
{
type HandledMessages = MappedHandledMessages<A::HandledMessages, M, R>;
fn handle_message(&mut self, msg: tytix_core::InternalMessage) {
match msg.into_inner::<M>() {
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<bool> = 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!");
}
}

97
flake.lock generated Normal file
View file

@ -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
}

96
flake.nix Normal file
View file

@ -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
];
};
}
);
}

2
rust-toolchain.toml Normal file
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "1.91.0"

3
rustfmt.toml Normal file
View file

@ -0,0 +1,3 @@
imports_granularity = "Item"
reorder_imports = true
group_imports = "StdExternalCrate"