Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Project Memory

- The CLI uses `clap` with `MergeOptions` to merge CLI args and config file settings.
- Prefer explicit flag naming (e.g., `sql_api_enabled` over `raw_sql`) to avoid ambiguity.
- Use `clap::ArgAction::Set` for boolean flags that accept explicit `true`/`false` values; avoid `SetTrue` in those cases.
- When a flag affects multiple transports (HTTP + gRPC), ensure the name/behavior applies consistently and is documented/tested.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"bin/torii",
"crates/broker",
"crates/cli",
"crates/cli-macros",
"crates/client",
"crates/messaging",
"crates/server",
Expand Down
14 changes: 14 additions & 0 deletions crates/cli-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "torii-cli-macros"
edition.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full"] }
127 changes: 127 additions & 0 deletions crates/cli-macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::Parser;
use syn::{parse_macro_input, parse_quote, Attribute, ItemStruct, LitStr, Meta};

#[proc_macro_attribute]
pub fn prefixed_args(attr: TokenStream, item: TokenStream) -> TokenStream {
let prefix = parse_prefix(attr);
let mut input = parse_macro_input!(item as ItemStruct);

if let syn::Fields::Named(fields) = &mut input.fields {
for field in fields.named.iter_mut() {
let mut skip = false;
let mut override_name: Option<String> = None;

let mut next_attrs: Vec<Attribute> = Vec::with_capacity(field.attrs.len());
for attr in field.attrs.drain(..) {
if attr.path().is_ident("prefixed_arg") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") {
skip = true;
return Ok(());
}
if meta.path.is_ident("rename") {
let value = meta.value()?;
let lit: LitStr = value.parse()?;
override_name = Some(lit.value());
return Ok(());
}
Ok(())
});
continue;
}

next_attrs.push(attr);
}
field.attrs = next_attrs;

if skip {
continue;
}

if has_arg_long(&field.attrs) {
continue;
}

let field_name = override_name
.or_else(|| serde_rename(&field.attrs))
.or_else(|| field.ident.as_ref().map(|ident| ident.to_string()));

let field_name = match field_name {
Some(name) => name,
None => continue,
};

let long_value = format!("{}.{}", prefix, field_name);
let long_lit = LitStr::new(&long_value, proc_macro2::Span::call_site());
let id_lit = LitStr::new(&long_value, proc_macro2::Span::call_site());
let long_attr: Attribute = parse_quote!(#[arg(long = #long_lit, id = #id_lit)]);
field.attrs.push(long_attr);
}
}

TokenStream::from(quote!(#input))
}

fn parse_prefix(attr: TokenStream) -> String {
let parser = syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated;
let meta = match parser.parse(attr) {
Ok(meta) => meta,
Err(_) => return String::new(),
};
for nested in meta {
if let Meta::NameValue(name_value) = nested {
if !name_value.path.is_ident("prefix") {
continue;
}
if let syn::Expr::Lit(expr_lit) = name_value.value {
if let syn::Lit::Str(lit_str) = expr_lit.lit {
return lit_str.value();
}
}
}
}

String::new()
}

fn has_arg_long(attrs: &[Attribute]) -> bool {
for attr in attrs {
if !attr.path().is_ident("arg") {
continue;
}
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("long") {
found = true;
}
Ok(())
});
if found {
return true;
}
}
false
}

fn serde_rename(attrs: &[Attribute]) -> Option<String> {
for attr in attrs {
if !attr.path().is_ident("serde") {
continue;
}
let mut rename_value = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
let value = meta.value()?;
let lit: LitStr = value.parse()?;
rename_value = Some(lit.value());
}
Ok(())
});
if rename_value.is_some() {
return rename_value;
}
}
None
}
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ torii-sqlite-types.workspace = true
url.workspace = true
merge-options.workspace = true
torii-proto.workspace = true
torii-cli-macros = { path = "../cli-macros" }

[dev-dependencies]
assert_matches.workspace = true
Expand Down
30 changes: 14 additions & 16 deletions crates/cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub struct ToriiArgs {
pub metrics: MetricsOptions,

#[cfg(feature = "server")]
#[serde(rename = "http", alias = "server")]
#[command(flatten)]
#[merge]
pub server: ServerOptions,
Expand Down Expand Up @@ -300,13 +301,13 @@ mod test {
assert_eq!(torii_args.sql.model_indices, vec![]);
assert_eq!(torii_args.sql.historical, Vec::<String>::new());

assert_eq!(torii_args.server.http_addr, DEFAULT_HTTP_ADDR);
assert_eq!(torii_args.server.http_port, DEFAULT_HTTP_PORT);
assert_eq!(torii_args.server.http_cors_origins, None);
assert_eq!(torii_args.server.addr, DEFAULT_HTTP_ADDR);
assert_eq!(torii_args.server.port, DEFAULT_HTTP_PORT);
assert_eq!(torii_args.server.cors_origins, None);

assert!(!torii_args.metrics.metrics);
assert_eq!(torii_args.metrics.metrics_addr, DEFAULT_METRICS_ADDR);
assert_eq!(torii_args.metrics.metrics_port, DEFAULT_METRICS_PORT);
assert!(!torii_args.metrics.enabled);
assert_eq!(torii_args.metrics.addr, DEFAULT_METRICS_ADDR);
assert_eq!(torii_args.metrics.port, DEFAULT_METRICS_PORT);

assert_eq!(torii_args.relay.port, DEFAULT_RELAY_PORT);
assert_eq!(torii_args.relay.webrtc_port, DEFAULT_RELAY_WEBRTC_PORT);
Expand All @@ -329,10 +330,10 @@ mod test {
[events]
raw = true

[server]
http_addr = "127.0.0.1"
http_port = 7777
http_cors_origins = ["*"]
[http]
addr = "127.0.0.1"
port = 7777
cors_origins = ["*"]

[indexing]
events_chunk_size = 9999
Expand Down Expand Up @@ -403,11 +404,8 @@ mod test {
fields: vec!["vec.x".to_string(), "vec.y".to_string()],
}]
);
assert_eq!(torii_args.server.http_addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert_eq!(torii_args.server.http_port, 7777);
assert_eq!(
torii_args.server.http_cors_origins,
Some(vec!["*".to_string()])
);
assert_eq!(torii_args.server.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
assert_eq!(torii_args.server.port, 7777);
assert_eq!(torii_args.server.cors_origins, Some(vec!["*".to_string()]));
}
}
Loading
Loading