Skip to content

Commit 5a64d76

Browse files
committed
feat(cli): derive clap names from config
1 parent e253a08 commit 5a64d76

File tree

8 files changed

+230
-140
lines changed

8 files changed

+230
-140
lines changed

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Project Memory
2+
3+
- The CLI uses `clap` with `MergeOptions` to merge CLI args and config file settings.
4+
- Prefer explicit flag naming (e.g., `sql_api_enabled` over `raw_sql`) to avoid ambiguity.
5+
- Use `clap::ArgAction::Set` for boolean flags that accept explicit `true`/`false` values; avoid `SetTrue` in those cases.
6+
- When a flag affects multiple transports (HTTP + gRPC), ensure the name/behavior applies consistently and is documented/tested.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ members = [
55
"bin/torii",
66
"crates/broker",
77
"crates/cli",
8+
"crates/cli-macros",
89
"crates/client",
910
"crates/messaging",
1011
"crates/server",

crates/cli-macros/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "torii-cli-macros"
3+
edition.workspace = true
4+
license.workspace = true
5+
repository.workspace = true
6+
version.workspace = true
7+
8+
[lib]
9+
proc-macro = true
10+
11+
[dependencies]
12+
proc-macro2 = "1.0"
13+
quote = "1.0"
14+
syn = { version = "2.0", features = ["full"] }

crates/cli-macros/src/lib.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
use proc_macro::TokenStream;
2+
use quote::quote;
3+
use syn::{parse_macro_input, parse_quote, Attribute, ItemStruct, LitStr, Meta, NestedMeta};
4+
5+
#[proc_macro_attribute]
6+
pub fn prefixed_args(attr: TokenStream, item: TokenStream) -> TokenStream {
7+
let prefix = parse_prefix(attr);
8+
let mut input = parse_macro_input!(item as ItemStruct);
9+
10+
if let syn::Fields::Named(fields) = &mut input.fields {
11+
for field in fields.named.iter_mut() {
12+
let mut skip = false;
13+
let mut override_name: Option<String> = None;
14+
15+
let mut next_attrs: Vec<Attribute> = Vec::with_capacity(field.attrs.len());
16+
for attr in field.attrs.drain(..) {
17+
if attr.path().is_ident("prefixed_arg") {
18+
let _ = attr.parse_nested_meta(|meta| {
19+
if meta.path.is_ident("skip") {
20+
skip = true;
21+
return Ok(());
22+
}
23+
if meta.path.is_ident("rename") {
24+
let value = meta.value()?;
25+
let lit: LitStr = value.parse()?;
26+
override_name = Some(lit.value());
27+
return Ok(());
28+
}
29+
Ok(())
30+
});
31+
continue;
32+
}
33+
34+
next_attrs.push(attr);
35+
}
36+
field.attrs = next_attrs;
37+
38+
if skip {
39+
continue;
40+
}
41+
42+
if has_arg_long(&field.attrs) {
43+
continue;
44+
}
45+
46+
let field_name = override_name
47+
.or_else(|| serde_rename(&field.attrs))
48+
.or_else(|| field.ident.as_ref().map(|ident| ident.to_string()));
49+
50+
let field_name = match field_name {
51+
Some(name) => name,
52+
None => continue,
53+
};
54+
55+
let long_value = format!("{}.{}", prefix, field_name);
56+
let long_lit = LitStr::new(&long_value, proc_macro2::Span::call_site());
57+
let long_attr: Attribute = parse_quote!(#[arg(long = #long_lit)]);
58+
field.attrs.push(long_attr);
59+
}
60+
}
61+
62+
TokenStream::from(quote!(#input))
63+
}
64+
65+
fn parse_prefix(attr: TokenStream) -> String {
66+
let meta = parse_macro_input!(attr as syn::AttributeArgs);
67+
for nested in meta {
68+
match nested {
69+
NestedMeta::Meta(Meta::NameValue(name_value)) if name_value.path.is_ident("prefix") => {
70+
if let syn::Expr::Lit(expr_lit) = name_value.value {
71+
if let syn::Lit::Str(lit_str) = expr_lit.lit {
72+
return lit_str.value();
73+
}
74+
}
75+
}
76+
NestedMeta::Lit(syn::Lit::Str(lit_str)) => {
77+
return lit_str.value();
78+
}
79+
_ => {}
80+
}
81+
}
82+
83+
String::new()
84+
}
85+
86+
fn has_arg_long(attrs: &[Attribute]) -> bool {
87+
for attr in attrs {
88+
if !attr.path().is_ident("arg") {
89+
continue;
90+
}
91+
let mut found = false;
92+
let _ = attr.parse_nested_meta(|meta| {
93+
if meta.path.is_ident("long") {
94+
found = true;
95+
}
96+
Ok(())
97+
});
98+
if found {
99+
return true;
100+
}
101+
}
102+
false
103+
}
104+
105+
fn serde_rename(attrs: &[Attribute]) -> Option<String> {
106+
for attr in attrs {
107+
if !attr.path().is_ident("serde") {
108+
continue;
109+
}
110+
let mut rename_value = None;
111+
let _ = attr.parse_nested_meta(|meta| {
112+
if meta.path.is_ident("rename") {
113+
let value = meta.value()?;
114+
let lit: LitStr = value.parse()?;
115+
rename_value = Some(lit.value());
116+
}
117+
Ok(())
118+
});
119+
if rename_value.is_some() {
120+
return rename_value;
121+
}
122+
}
123+
None
124+
}

crates/cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ torii-sqlite-types.workspace = true
1818
url.workspace = true
1919
merge-options.workspace = true
2020
torii-proto.workspace = true
21+
torii-cli-macros = { path = \"../cli-macros\" }
2122

2223
[dev-dependencies]
2324
assert_matches.workspace = true

crates/cli/src/args.rs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ pub struct ToriiArgs {
8080
pub metrics: MetricsOptions,
8181

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

303-
assert_eq!(torii_args.server.http_addr, DEFAULT_HTTP_ADDR);
304-
assert_eq!(torii_args.server.http_port, DEFAULT_HTTP_PORT);
305-
assert_eq!(torii_args.server.http_cors_origins, None);
304+
assert_eq!(torii_args.server.addr, DEFAULT_HTTP_ADDR);
305+
assert_eq!(torii_args.server.port, DEFAULT_HTTP_PORT);
306+
assert_eq!(torii_args.server.cors_origins, None);
306307

307-
assert!(!torii_args.metrics.metrics);
308-
assert_eq!(torii_args.metrics.metrics_addr, DEFAULT_METRICS_ADDR);
309-
assert_eq!(torii_args.metrics.metrics_port, DEFAULT_METRICS_PORT);
308+
assert!(!torii_args.metrics.enabled);
309+
assert_eq!(torii_args.metrics.addr, DEFAULT_METRICS_ADDR);
310+
assert_eq!(torii_args.metrics.port, DEFAULT_METRICS_PORT);
310311

311312
assert_eq!(torii_args.relay.port, DEFAULT_RELAY_PORT);
312313
assert_eq!(torii_args.relay.webrtc_port, DEFAULT_RELAY_WEBRTC_PORT);
@@ -329,10 +330,10 @@ mod test {
329330
[events]
330331
raw = true
331332
332-
[server]
333-
http_addr = "127.0.0.1"
334-
http_port = 7777
335-
http_cors_origins = ["*"]
333+
[http]
334+
addr = "127.0.0.1"
335+
port = 7777
336+
cors_origins = ["*"]
336337
337338
[indexing]
338339
events_chunk_size = 9999
@@ -403,10 +404,10 @@ mod test {
403404
fields: vec!["vec.x".to_string(), "vec.y".to_string()],
404405
}]
405406
);
406-
assert_eq!(torii_args.server.http_addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
407-
assert_eq!(torii_args.server.http_port, 7777);
407+
assert_eq!(torii_args.server.addr, IpAddr::V4(Ipv4Addr::LOCALHOST));
408+
assert_eq!(torii_args.server.port, 7777);
408409
assert_eq!(
409-
torii_args.server.http_cors_origins,
410+
torii_args.server.cors_origins,
410411
Some(vec!["*".to_string()])
411412
);
412413
}

0 commit comments

Comments
 (0)