#![feature(closure_lifetime_binder)]
#![cfg(feature = "macros")]
use cmdreg::*;
#[command("test.macro")]
fn ping() -> CommandResult {
CommandResponse::json("pong")
}
#[command("test.macro")]
async fn async_hello() -> CommandResult {
CommandResponse::json("hello async")
}
#[command]
fn bare_cmd() -> CommandResult {
CommandResponse::json("bare")
}
#[command]
async fn bare_async_cmd() -> CommandResult {
CommandResponse::json("bare async")
}
#[command("test.macro")]
fn greet(Json(name): Json<String>) -> CommandResult {
CommandResponse::json(format!("hi, {}", name))
}
#[command("test.macro")]
async fn async_greet(Json(name): Json<String>) -> CommandResult {
CommandResponse::json(format!("hello, {}", name))
}
#[command("test.macro")]
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[command("test.macro")]
fn echo(name: String) -> String {
format!("echo: {}", name)
}
#[command("test.macro")]
async fn async_echo(name: String) -> String {
format!("async echo: {}", name)
}
#[derive(serde::Deserialize)]
struct Point {
x: f64,
y: f64,
}
#[command("test.macro")]
fn point_len(p: Point) -> f64 {
(p.x * p.x + p.y * p.y).sqrt()
}
#[command("test.macro")]
async fn async_multiply(x: i32, y: i32) -> i32 {
x * y
}
#[command]
fn get_version() -> String {
"1.0.0".to_string()
}
#[command]
async fn async_get_version() -> String {
"2.0.0".to_string()
}
#[command("test.macro")]
fn safe_divide(a: f64, b: f64) -> anyhow::Result<f64> {
if b == 0.0 {
anyhow::bail!("division by zero");
}
Ok(a / b)
}
#[command("test.macro")]
fn do_nothing() {}
#[command("test.classic")]
fn classic_bool(Json(name): Json<String>) -> bool {
name == "yes"
}
#[command("test.classic")]
async fn async_classic_string(Json(n): Json<i32>) -> String {
format!("value: {}", n)
}
#[derive(serde::Serialize, serde::Deserialize)]
struct Pair {
left: String,
right: String,
}
#[command("test.classic")]
fn classic_result(Json(pair): Json<Pair>) -> anyhow::Result<String> {
if pair.left.is_empty() {
anyhow::bail!("left is empty");
}
Ok(format!("{}-{}", pair.left, pair.right))
}
#[command("test.classic")]
fn classic_unit(Json(_n): Json<i32>) {}
#[command("test.macro")]
fn with_raw_idents(r#type: String, r#move: bool) -> String {
format!("type={}, move={}", r#type, r#move)
}
#[command("test.macro")]
async fn async_with_raw_idents(r#type: String, r#override: bool) -> String {
format!("type={}, override={}", r#type, r#override)
}
#[command("test.macro", rename_all = "camelCase")]
fn with_camel_case(file_path: String, is_recursive: bool) -> String {
format!("path={}, recursive={}", file_path, is_recursive)
}
#[command(rename_all = "SCREAMING_SNAKE_CASE")]
fn with_screaming_snake(my_value: i32) -> i32 {
my_value * 2
}
#[command("test.global")]
fn global_rename_test(file_path: String, is_recursive: bool) -> String {
format!("path={}, recursive={}", file_path, is_recursive)
}
#[command("test.global")]
async fn async_global_rename_test(max_depth: i32, include_hidden: bool) -> String {
format!("depth={}, hidden={}", max_depth, include_hidden)
}
#[test]
fn test_macro_reg_all_commands() {
reg_all_commands().unwrap();
let keys = get_command_keys().unwrap();
assert!(keys.contains(&"test.macro.ping".to_string()));
assert!(keys.contains(&"test.macro.greet".to_string()));
assert!(keys.contains(&"bare_cmd".to_string()));
}
#[test]
fn test_macro_sync_invoke_prefixed() {
reg_all_commands().unwrap();
let result = invoke_command("test.macro.ping", CommandContext::None).unwrap();
assert_eq!(result.into_option().unwrap(), r#""pong""#);
}
#[test]
fn test_macro_sync_invoke_bare() {
reg_all_commands().unwrap();
let result = invoke_command("bare_cmd", CommandContext::None).unwrap();
assert_eq!(result.into_option().unwrap(), r#""bare""#);
}
#[test]
fn test_macro_sync_invoke_with_args() {
reg_all_commands().unwrap();
let args = serde_json::to_string("World").unwrap();
let result = invoke_command("test.macro.greet", CommandContext::String(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), r#""hi, World""#);
}
#[tokio::test]
async fn test_macro_async_invoke_prefixed() {
tokio::task::spawn_blocking(|| reg_all_commands().unwrap())
.await
.unwrap();
let result = invoke_command_async("test.macro.async_hello", CommandContext::None)
.await
.unwrap();
assert_eq!(result.into_option().unwrap(), r#""hello async""#);
}
#[tokio::test]
async fn test_macro_async_invoke_bare() {
tokio::task::spawn_blocking(|| reg_all_commands().unwrap())
.await
.unwrap();
let result = invoke_command_async("bare_async_cmd", CommandContext::None)
.await
.unwrap();
assert_eq!(result.into_option().unwrap(), r#""bare async""#);
}
#[tokio::test]
async fn test_macro_async_invoke_with_args() {
tokio::task::spawn_blocking(|| reg_all_commands().unwrap())
.await
.unwrap();
let args = serde_json::to_string("Alice").unwrap();
let result = invoke_command_async("test.macro.async_greet", CommandContext::String(&args))
.await
.unwrap();
assert_eq!(result.into_option().unwrap(), r#""hello, Alice""#);
}
#[test]
fn test_macro_plain_sync_add() {
reg_all_commands().unwrap();
let args = serde_json::json!({"a": 3, "b": 4});
let result = invoke_command("test.macro.add", CommandContext::Value(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), "7");
}
#[tokio::test]
async fn test_macro_plain_async_multiply() {
tokio::task::spawn_blocking(|| reg_all_commands().unwrap())
.await
.unwrap();
let args = serde_json::json!({"x": 5, "y": 6});
let result = invoke_command_async("test.macro.async_multiply", CommandContext::Value(&args))
.await
.unwrap();
assert_eq!(result.into_option().unwrap(), "30");
}
#[test]
fn test_macro_plain_no_params_with_return() {
reg_all_commands().unwrap();
let result = invoke_command("get_version", CommandContext::None).unwrap();
assert_eq!(result.into_option().unwrap(), r#""1.0.0""#);
}
#[tokio::test]
async fn test_macro_plain_async_no_params_with_return() {
tokio::task::spawn_blocking(|| reg_all_commands().unwrap())
.await
.unwrap();
let result = invoke_command_async("async_get_version", CommandContext::None)
.await
.unwrap();
assert_eq!(result.into_option().unwrap(), r#""2.0.0""#);
}
#[test]
fn test_macro_plain_result_return() {
reg_all_commands().unwrap();
let args = serde_json::json!({"a": 10.0, "b": 2.0});
let result = invoke_command("test.macro.safe_divide", CommandContext::Value(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), "5.0");
let args = serde_json::json!({"a": 10.0, "b": 0.0});
let result = invoke_command("test.macro.safe_divide", CommandContext::Value(&args));
assert!(result.is_err());
}
#[test]
fn test_macro_plain_unit_return() {
reg_all_commands().unwrap();
let result = invoke_command("test.macro.do_nothing", CommandContext::None).unwrap();
assert!(result.is_none());
}
#[test]
fn test_macro_plain_single_param_sync() {
reg_all_commands().unwrap();
let args = serde_json::json!({"name": "World"});
let result = invoke_command("test.macro.echo", CommandContext::Value(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), r#""echo: World""#);
}
#[tokio::test]
async fn test_macro_plain_single_param_async() {
tokio::task::spawn_blocking(|| reg_all_commands().unwrap())
.await
.unwrap();
let args = serde_json::json!({"name": "Alice"});
let result = invoke_command_async("test.macro.async_echo", CommandContext::Value(&args))
.await
.unwrap();
assert_eq!(result.into_option().unwrap(), r#""async echo: Alice""#);
}
#[test]
fn test_macro_plain_single_struct_param() {
reg_all_commands().unwrap();
let args = serde_json::json!({"p": {"x": 3.0, "y": 4.0}});
let result = invoke_command("test.macro.point_len", CommandContext::Value(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), "5.0");
}
#[test]
fn test_classic_bool_return() {
reg_all_commands().unwrap();
let args = serde_json::to_string("yes").unwrap();
let result =
invoke_command("test.classic.classic_bool", CommandContext::String(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), "true");
let args = serde_json::to_string("no").unwrap();
let result =
invoke_command("test.classic.classic_bool", CommandContext::String(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), "false");
}
#[tokio::test]
async fn test_classic_async_string_return() {
tokio::task::spawn_blocking(|| reg_all_commands().unwrap())
.await
.unwrap();
let args = serde_json::to_string(&42).unwrap();
let result = invoke_command_async(
"test.classic.async_classic_string",
CommandContext::String(&args),
)
.await
.unwrap();
assert_eq!(result.into_option().unwrap(), r#""value: 42""#);
}
#[test]
fn test_classic_result_return() {
reg_all_commands().unwrap();
let args = serde_json::json!({"left": "a", "right": "b"});
let result =
invoke_command("test.classic.classic_result", CommandContext::Value(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), r#""a-b""#);
let args = serde_json::json!({"left": "", "right": "b"});
let result = invoke_command("test.classic.classic_result", CommandContext::Value(&args));
assert!(result.is_err());
}
#[test]
fn test_classic_unit_return() {
reg_all_commands().unwrap();
let args = serde_json::to_string(&42).unwrap();
let result =
invoke_command("test.classic.classic_unit", CommandContext::String(&args)).unwrap();
assert!(result.is_none());
}
#[test]
fn test_macro_raw_idents_sync() {
reg_all_commands().unwrap();
let args = serde_json::json!({"type": "file", "move": true});
let result =
invoke_command("test.macro.with_raw_idents", CommandContext::Value(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), r#""type=file, move=true""#);
}
#[tokio::test]
async fn test_macro_raw_idents_async() {
tokio::task::spawn_blocking(|| reg_all_commands().unwrap())
.await
.unwrap();
let args = serde_json::json!({"type": "dir", "override": false});
let result = invoke_command_async(
"test.macro.async_with_raw_idents",
CommandContext::Value(&args),
)
.await
.unwrap();
assert_eq!(
result.into_option().unwrap(),
r#""type=dir, override=false""#
);
}
#[test]
fn test_macro_rename_all_camel_case() {
reg_all_commands().unwrap();
let args = serde_json::json!({"filePath": "/tmp", "isRecursive": true});
let result =
invoke_command("test.macro.with_camel_case", CommandContext::Value(&args)).unwrap();
assert_eq!(
result.into_option().unwrap(),
r#""path=/tmp, recursive=true""#
);
}
#[test]
fn test_macro_rename_all_screaming_snake() {
reg_all_commands().unwrap();
let args = serde_json::json!({"MY_VALUE": 21});
let result = invoke_command("with_screaming_snake", CommandContext::Value(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), "42");
}
#[test]
fn test_macro_no_rename_default() {
reg_all_commands().unwrap();
let args = serde_json::json!({"a": 10, "b": 20});
let result = invoke_command("test.macro.add", CommandContext::Value(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), "30");
}
#[test]
fn test_macro_global_rename_all() {
reg_all_commands().unwrap();
let args = serde_json::json!({"filePath": "/tmp", "isRecursive": true});
let result = invoke_command(
"test.global.global_rename_test",
CommandContext::Value(&args),
)
.unwrap();
assert_eq!(
result.into_option().unwrap(),
r#""path=/tmp, recursive=true""#
);
}
#[tokio::test]
async fn test_macro_global_rename_all_async() {
tokio::task::spawn_blocking(|| reg_all_commands().unwrap())
.await
.unwrap();
let args = serde_json::json!({"maxDepth": 5, "includeHidden": false});
let result = invoke_command_async(
"test.global.async_global_rename_test",
CommandContext::Value(&args),
)
.await
.unwrap();
assert_eq!(result.into_option().unwrap(), r#""depth=5, hidden=false""#);
}
#[test]
fn test_macro_explicit_rename_overrides_global() {
reg_all_commands().unwrap();
let args = serde_json::json!({"MY_VALUE": 21});
let result = invoke_command("with_screaming_snake", CommandContext::Value(&args)).unwrap();
assert_eq!(result.into_option().unwrap(), "42");
}
#[cfg(feature = "metadata")]
#[test]
fn test_get_all_command_metas() {
reg_all_commands().unwrap();
let metas = get_all_command_metas();
assert!(!metas.is_empty());
let add_meta = metas.iter().find(|m| m.name == "test.macro.add").unwrap();
assert_eq!(add_meta.style, "plain");
assert!(!add_meta.is_async);
assert_eq!(add_meta.params.len(), 2);
assert_eq!(add_meta.params[0].name, "a");
assert_eq!(add_meta.params[1].name, "b");
let async_mul = metas
.iter()
.find(|m| m.name == "test.macro.async_multiply")
.unwrap();
assert_eq!(async_mul.style, "plain");
assert!(async_mul.is_async);
assert_eq!(async_mul.params.len(), 2);
let ping_meta = metas.iter().find(|m| m.name == "test.macro.ping").unwrap();
assert_eq!(ping_meta.style, "classic");
assert!(ping_meta.params.is_empty());
let ver_meta = metas.iter().find(|m| m.name == "get_version").unwrap();
assert_eq!(ver_meta.style, "classic");
assert!(ver_meta.params.is_empty());
}
#[cfg(feature = "metadata")]
#[test]
fn test_export_commands_json() {
reg_all_commands().unwrap();
let path = std::env::temp_dir().join("cmdreg_test_commands.json");
export_commands_json(&path).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&content).unwrap();
assert!(parsed.is_array());
let arr = parsed.as_array().unwrap();
assert!(!arr.is_empty());
let add_entry = arr.iter().find(|v| v["name"] == "test.macro.add").unwrap();
assert_eq!(add_entry["style"], "plain");
assert_eq!(add_entry["is_async"], false);
assert!(add_entry["params"].is_array());
assert_eq!(add_entry["params"][0]["name"], "a");
std::fs::remove_file(&path).ok();
}