Basic example to try the harmony! macro

This commit is contained in:
Ian Letourneau
2025-11-10 11:50:22 -05:00
commit cc6da434c7
9 changed files with 377 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

103
Cargo.lock generated Normal file
View File

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

3
Cargo.toml Normal file
View File

@@ -0,0 +1,3 @@
[workspace]
resolver = "2"
members = ["examples/hello_world", "harmony", "harmony_macros"]

View File

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

View File

@@ -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<T: Topology> Score<T> for MyScore {
fn name(&self) -> String {
"MyScore".to_string()
}
#[doc(hidden)]
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(MyInterpret {})
}
}
#[derive(Debug)]
struct MyInterpret {}
#[async_trait]
impl<T: Topology> Interpret<T> for MyInterpret {
async fn execute(&self, _inventory: &Inventory, _topology: &T) -> Result<(), String> {
println!("MyInterpret is executing");
Ok(())
}
}
struct AnotherScore {}
#[async_trait]
impl<T: Topology> Score<T> for AnotherScore {
fn name(&self) -> String {
"AnotherScore".to_string()
}
#[doc(hidden)]
fn create_interpret(&self) -> Box<dyn Interpret<T>> {
Box::new(AnotherInterpret {})
}
}
#[derive(Debug)]
struct AnotherInterpret {}
#[async_trait]
impl<T: Topology> Interpret<T> 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 {}
]
}

9
harmony/Cargo.toml Normal file
View File

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

43
harmony/src/lib.rs Normal file
View File

@@ -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<T: Topology>: 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<dyn Interpret<T>>;
}
#[async_trait]
pub trait Interpret<T>: std::fmt::Debug + Send {
async fn execute(&self, inventory: &Inventory, topology: &T) -> Result<(), String>;
}
pub async fn run_cli<T: Topology>(
inventory: Inventory,
topology: T,
scores: Vec<Box<dyn Score<T>>>,
) -> Result<(), String> {
for score in scores {
let interpret = score.create_interpret();
interpret.execute(&inventory, &topology).await?;
}
Ok(())
}

11
harmony_macros/Cargo.toml Normal file
View File

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

118
harmony_macros/src/lib.rs Normal file
View File

@@ -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<Expr, token::Comma>,
}
// Custom parser for `key: value,` syntax
impl syn::parse::Parse for HarmonyInput {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
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::<token::Colon>()?;
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::<token::Comma>();
}
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<T: harmony::Topology>(
// `topology` is passed to pin down the type `T`
_topology: &T,
) -> Vec<Box<dyn harmony::Score<T>>> {
let mut v: Vec<Box<dyn harmony::Score<T>>> = 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)
}