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