From cc6da434c7bc6bfbc2c1d42667aefa8b82983a7f Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Mon, 10 Nov 2025 11:50:22 -0500 Subject: [PATCH] Basic example to try the harmony! macro --- .gitignore | 1 + Cargo.lock | 103 +++++++++++++++++++++++++++ Cargo.toml | 3 + examples/hello_world/Cargo.toml | 14 ++++ examples/hello_world/src/main.rs | 75 ++++++++++++++++++++ harmony/Cargo.toml | 9 +++ harmony/src/lib.rs | 43 +++++++++++ harmony_macros/Cargo.toml | 11 +++ harmony_macros/src/lib.rs | 118 +++++++++++++++++++++++++++++++ 9 files changed, 377 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 examples/hello_world/Cargo.toml create mode 100644 examples/hello_world/src/main.rs create mode 100644 harmony/Cargo.toml create mode 100644 harmony/src/lib.rs create mode 100644 harmony_macros/Cargo.toml create mode 100644 harmony_macros/src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fc6878a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,103 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "harmony" +version = "0.1.0" +dependencies = [ + "async-trait", + "harmony_macros", + "tokio", +] + +[[package]] +name = "harmony_macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "hello_world" +version = "0.1.0" +dependencies = [ + "async-trait", + "harmony", + "harmony_macros", + "tokio", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4a50919 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["examples/hello_world", "harmony", "harmony_macros"] diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml new file mode 100644 index 0000000..c5810a9 --- /dev/null +++ b/examples/hello_world/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "hello_world" +version = "0.1.0" +edition = "2024" + +[[example]] +name = "hello_world" +path = "src/main.rs" + +[dependencies] +async-trait = "0.1.89" +harmony = { version = "0.1.0", path = "../../harmony" } +harmony_macros = { version = "0.1.0", path = "../../harmony_macros" } +tokio = "1.48.0" diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs new file mode 100644 index 0000000..43d558d --- /dev/null +++ b/examples/hello_world/src/main.rs @@ -0,0 +1,75 @@ +use async_trait::async_trait; +use harmony::{Interpret, Inventory, Score, Topology}; +use harmony_macros::harmony; + +struct MyTopology {} + +#[async_trait] +impl Topology for MyTopology { + fn name(&self) -> &str { + "MyTopology" + } + + async fn ensure_ready(&self) -> Result<(), String> { + Ok(()) + } +} + +struct MyScore {} + +#[async_trait] +impl Score for MyScore { + fn name(&self) -> String { + "MyScore".to_string() + } + + #[doc(hidden)] + fn create_interpret(&self) -> Box> { + Box::new(MyInterpret {}) + } +} + +#[derive(Debug)] +struct MyInterpret {} + +#[async_trait] +impl Interpret for MyInterpret { + async fn execute(&self, _inventory: &Inventory, _topology: &T) -> Result<(), String> { + println!("MyInterpret is executing"); + Ok(()) + } +} + +struct AnotherScore {} + +#[async_trait] +impl Score for AnotherScore { + fn name(&self) -> String { + "AnotherScore".to_string() + } + + #[doc(hidden)] + fn create_interpret(&self) -> Box> { + Box::new(AnotherInterpret {}) + } +} + +#[derive(Debug)] +struct AnotherInterpret {} + +#[async_trait] +impl Interpret for AnotherInterpret { + async fn execute(&self, _inventory: &Inventory, _topology: &T) -> Result<(), String> { + println!("AnotherInterpret is executing"); + Ok(()) + } +} + +harmony! { + inventory: Inventory { location: "hello".to_string()}, + topology: MyTopology {} + scores: [ + MyScore {}, + AnotherScore {} + ] +} diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml new file mode 100644 index 0000000..9790423 --- /dev/null +++ b/harmony/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "harmony" +version = "0.1.0" +edition = "2024" + +[dependencies] +harmony_macros = { version = "0.1.0", path = "../harmony_macros" } +async-trait = "0.1.89" +tokio = { version = "1.48.0", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/harmony/src/lib.rs b/harmony/src/lib.rs new file mode 100644 index 0000000..c037d29 --- /dev/null +++ b/harmony/src/lib.rs @@ -0,0 +1,43 @@ +use async_trait::async_trait; + +#[async_trait] +pub trait Topology: Send + Sync { + fn name(&self) -> &str; + async fn ensure_ready(&self) -> Result<(), String>; +} + +#[derive(Debug)] +pub struct Inventory { + pub location: String, +} + +#[async_trait] +pub trait Score: Send + Sync { + async fn interpret(&self, inventory: &Inventory, topology: &T) -> Result<(), String> { + let interpret = self.create_interpret(); + interpret.execute(inventory, topology).await + } + + fn name(&self) -> String; + + #[doc(hidden)] + fn create_interpret(&self) -> Box>; +} + +#[async_trait] +pub trait Interpret: std::fmt::Debug + Send { + async fn execute(&self, inventory: &Inventory, topology: &T) -> Result<(), String>; +} + +pub async fn run_cli( + inventory: Inventory, + topology: T, + scores: Vec>>, +) -> Result<(), String> { + for score in scores { + let interpret = score.create_interpret(); + interpret.execute(&inventory, &topology).await?; + } + + Ok(()) +} diff --git a/harmony_macros/Cargo.toml b/harmony_macros/Cargo.toml new file mode 100644 index 0000000..8ecdaf3 --- /dev/null +++ b/harmony_macros/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "harmony_macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.42" +syn = "2.0.109" diff --git a/harmony_macros/src/lib.rs b/harmony_macros/src/lib.rs new file mode 100644 index 0000000..7e6a3f8 --- /dev/null +++ b/harmony_macros/src/lib.rs @@ -0,0 +1,118 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{Expr, Ident, punctuated::Punctuated, token}; + +// Define the struct for our macro's input DSL +struct HarmonyInput { + inventory_expr: Expr, + topology_expr: Expr, + scores_array: Punctuated, +} + +// Custom parser for `key: value,` syntax +impl syn::parse::Parse for HarmonyInput { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut inventory_expr = None; + let mut topology_expr = None; + let mut scores_array = None; + + while !input.is_empty() { + let key: Ident = input.parse()?; + input.parse::()?; + + if key == "inventory" { + inventory_expr = Some(input.parse()?); + } else if key == "topology" { + topology_expr = Some(input.parse()?); + } else if key == "scores" { + let content; + syn::bracketed!(content in input); + scores_array = Some(Punctuated::parse_terminated(&content)?); + } else { + return Err(syn::Error::new(key.span(), "unknown key")); + } + // Eat the optional trailing comma + let _ = input.parse::(); + } + + Ok(HarmonyInput { + inventory_expr: inventory_expr.ok_or_else(|| input.error("missing 'inventory' key"))?, + topology_expr: topology_expr.ok_or_else(|| input.error("missing 'topology' key"))?, + scores_array: scores_array.ok_or_else(|| input.error("missing 'scores' key"))?, + }) + } +} + +/// The main Harmony entrypoint macro. +/// +/// This generates the `fn main()` and `tokio` runtime, +/// and calls `harmony::run_cli` with your provided +/// `inventory`, `topology`, and `scores`. +#[proc_macro] +pub fn harmony(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as HarmonyInput); + + let inventory_expr = input.inventory_expr; + let topology_expr = input.topology_expr; + + // Create the `Box::new()` for each score + let boxed_scores = input.scores_array.iter().map(|score_expr| { + quote! { Box::new(#score_expr) } + }); + + // Generate the `fn main()` + let expanded = quote! { + // This is the auto-generated main + fn main() { + // 1. Setup the Tokio runtime + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("Failed to build Tokio runtime"); + + runtime.block_on(async { + // 3. Evaluate the user's expressions + let inventory = #inventory_expr; + let topology = #topology_expr; + + // 4. This helper function is the key. + // It lets the compiler infer `T` from `topology` and + // type-check the `scores` against it. + fn __harmony_collect_scores( + // `topology` is passed to pin down the type `T` + _topology: &T, + ) -> Vec>> { + let mut v: Vec>> = Vec::new(); + + #( + v.push(#boxed_scores); + )* + + v + } + + // 5. Create the scores vector + let scores_vec = { + __harmony_collect_scores(&topology) + }; + + // 6. Call the *real* run function + let result = harmony::run_cli( + inventory, + topology, + scores_vec, + ).await; + + // 7. Handle the final result + if let Err(e) = result { + eprintln!("\nError: {}", e); + std::process::exit(1); + } + }); + } + }; + + TokenStream::from(expanded) +}