Basic example to try the harmony! macro
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
103
Cargo.lock
generated
Normal file
103
Cargo.lock
generated
Normal 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
3
Cargo.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = ["examples/hello_world", "harmony", "harmony_macros"]
|
||||||
14
examples/hello_world/Cargo.toml
Normal file
14
examples/hello_world/Cargo.toml
Normal 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"
|
||||||
75
examples/hello_world/src/main.rs
Normal file
75
examples/hello_world/src/main.rs
Normal 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
9
harmony/Cargo.toml
Normal 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
43
harmony/src/lib.rs
Normal 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
11
harmony_macros/Cargo.toml
Normal 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
118
harmony_macros/src/lib.rs
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user