use heck::ToKebabCase;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use server_less_parse::{
MethodInfo, ParamInfo, extract_groups, extract_map_type, extract_methods, extract_option_type,
extract_vec_type, get_impl_name, is_unit_type, partition_methods, resolve_method_group,
};
use syn::{ItemImpl, Token, parse::Parse};
use crate::context::{
generate_cli_context_extraction, partition_context_params,
};
use crate::app::extract_app_meta;
use crate::server_attrs::{has_server_hidden, has_server_skip, validate_server_attrs};
#[derive(Default)]
pub(crate) struct CliArgs {
pub name: Option<String>,
pub version: Option<String>,
pub description: Option<String>,
pub homepage: Option<String>,
pub global: Vec<(String, Option<String>)>,
pub defaults: Option<String>,
pub no_sync: bool,
pub no_async: bool,
pub config_ty: Option<syn::Path>,
pub config_cmd_name: Option<String>,
pub description_prefix: Option<bool>,
pub manual: Option<bool>,
pub input_schema: Option<bool>,
pub output_schema: Option<bool>,
}
#[derive(Clone, Copy)]
pub(crate) struct MetaFlags {
pub manual: bool,
pub input_schema: bool,
pub output_schema: bool,
}
impl Parse for CliArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut args = CliArgs::default();
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
match ident.to_string().as_str() {
"no_sync" => {
args.no_sync = true;
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
continue;
}
"no_async" => {
args.no_async = true;
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
continue;
}
"description_prefix" if !input.peek(Token![=]) => {
args.description_prefix = Some(true);
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
continue;
}
_ => {}
}
input.parse::<Token![=]>()?;
match ident.to_string().as_str() {
"name" => {
let lit: syn::LitStr = input.parse()?;
args.name = Some(lit.value());
}
"version" => {
let lit: syn::LitStr = input.parse()?;
args.version = Some(lit.value());
}
"description" => {
let lit: syn::LitStr = input.parse()?;
args.description = Some(lit.value());
}
"homepage" => {
let lit: syn::LitStr = input.parse()?;
args.homepage = Some(lit.value());
}
"global" => {
let content;
syn::bracketed!(content in input);
while !content.is_empty() {
let flag: syn::Ident = content.parse()?;
let help = if content.peek(Token![=]) {
content.parse::<Token![=]>()?;
let lit: syn::LitStr = content.parse()?;
Some(lit.value())
} else {
None
};
args.global.push((flag.to_string(), help));
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
}
"defaults" => {
let lit: syn::LitStr = input.parse()?;
args.defaults = Some(lit.value());
}
"description_prefix" => {
let lit: syn::LitBool = input.parse()?;
args.description_prefix = Some(lit.value());
}
"manual" => {
let lit: syn::LitBool = input.parse()?;
args.manual = Some(lit.value());
}
"input_schema" => {
let lit: syn::LitBool = input.parse()?;
args.input_schema = Some(lit.value());
}
"output_schema" => {
let lit: syn::LitBool = input.parse()?;
args.output_schema = Some(lit.value());
}
other => {
if other == "about" {
return Err(syn::Error::new(
ident.span(),
"unknown argument `about` — renamed to `description` in 0.4.0\n\
\n\
Example: #[cli(description = \"My CLI tool\")]",
));
}
if other == "name_prefix" {
return Err(syn::Error::new(
ident.span(),
"unknown argument `name_prefix` — renamed to `description_prefix`\n\
\n\
Example: #[cli(description_prefix = false)]",
));
}
const VALID: &[&str] = &[
"name", "version", "description", "homepage", "global",
"defaults", "no_sync", "no_async", "description_prefix",
"manual", "input_schema", "output_schema",
];
let suggestion = crate::did_you_mean(other, VALID)
.map(|s| format!(" — did you mean `{s}`?"))
.unwrap_or_default();
return Err(syn::Error::new(
ident.span(),
format!(
"unknown argument `{other}`{suggestion}\n\
\n\
Valid arguments: name, version, description, homepage, global, defaults, no_sync, no_async, description_prefix, manual, input_schema, output_schema\n\
\n\
Example: #[cli(name = \"my-app\", description = \"My CLI tool\")]\n\
Bare flags: #[cli(no_sync)] or #[cli(no_async)]\n\
\n\
Related: #[program] preset (CLI + markdown docs), #[markdown] (standalone docs)"
),
));
}
}
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(args)
}
}
fn has_cli_skip(method: &MethodInfo) -> bool {
if has_server_skip(method) {
return true;
}
for attr in &method.method.attrs {
if attr.path().is_ident("cli") {
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("skip") || meta.path.is_ident("helper") {
found = true;
}
if meta.input.peek(syn::Token![=]) {
let _: proc_macro2::TokenStream = meta.value()?.parse()?;
}
Ok(())
});
if found {
return true;
}
}
}
false
}
fn has_cli_default(method: &MethodInfo) -> bool {
for attr in &method.method.attrs {
if attr.path().is_ident("cli") {
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("default") {
found = true;
}
if meta.input.peek(syn::Token![=]) {
let _: proc_macro2::TokenStream = meta.value()?.parse()?;
}
Ok(())
});
if found {
return true;
}
}
}
false
}
fn has_cli_manual_false(method: &MethodInfo) -> bool {
for attr in &method.method.attrs {
if attr.path().is_ident("cli") {
let mut disabled = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("manual") && meta.input.peek(syn::Token![=]) {
let value = meta.value()?;
let lit: syn::LitBool = value.parse()?;
if !lit.value() {
disabled = true;
}
} else if meta.input.peek(syn::Token![=]) {
let _: syn::Expr = meta.value()?.parse()?;
}
Ok(())
});
if disabled {
return true;
}
}
}
false
}
fn check_reserved_flag_collisions(
partitioned: &server_less_parse::PartitionedMethods,
meta: MetaFlags,
global_flags: &[String],
) -> syn::Result<()> {
let reserved_for = |surfaces: MetaFlags| -> Vec<(&'static str, &'static str)> {
let mut r: Vec<(&str, &str)> = vec![
("json", "the --json output format"),
("jsonl", "the --jsonl output format"),
("jq", "the --jq output filter"),
("params-json", "the --params-json bulk input flag"),
];
if surfaces.manual {
r.push((
"manual",
"the --manual reference surface (disable with #[cli(manual = false)])",
));
}
if surfaces.input_schema {
r.push((
"input-schema",
"the --input-schema surface (disable with #[cli(input_schema = false)])",
));
}
if surfaces.output_schema {
r.push((
"output-schema",
"the --output-schema surface (disable with #[cli(output_schema = false)])",
));
}
r
};
let all_on = MetaFlags { manual: true, input_schema: true, output_schema: true };
let leaf_reserved = reserved_for(meta);
let slug_reserved = reserved_for(all_on);
let global_reserved: Vec<(String, &'static str)> = global_flags
.iter()
.map(|g| {
(
g.replace('_', "-"),
"a declared `global = [...]` flag — delivered via the CliGlobals sink, \
not method params",
)
})
.collect();
let groups = [
(&partitioned.leaf, &leaf_reserved),
(&partitioned.slug_mounts, &slug_reserved),
];
for (methods, reserved) in groups {
for m in methods.iter() {
let (_, regular) = partition_context_params(&m.params)?;
for p in ®ular {
let kebab = cli_param_name(p);
if let Some((flag, what)) = reserved.iter().find(|(name, _)| *name == kebab) {
return Err(syn::Error::new(
p.name.span(),
format!(
"parameter `{param}` collides with the injected `--{flag}` global flag — {what}\n\
\n\
Each #[cli] command receives built-in global flags; a parameter whose \
flag name matches one would make clap panic at runtime. Rename the Rust \
parameter, or disable the conflicting meta-surface (e.g. \
#[cli(manual = false)]).",
param = p.name_str(),
),
));
}
if let Some((flag, what)) =
global_reserved.iter().find(|(name, _)| name == &kebab)
{
return Err(syn::Error::new(
p.name.span(),
format!(
"parameter `{param}` collides with `--{flag}` — {what}\n\
\n\
A declared `global = [...]` flag is registered on the root with \
`.global(true)` and is delivered only through the `CliGlobals` sink \
(`set_global_flag`). A method parameter that shares its flag name would \
collide with the root flag at clap-build time and would never be \
auto-filled from the global. Rename the parameter, and read the global's \
value from your `CliGlobals` impl instead.",
param = p.name_str(),
),
));
}
}
}
}
Ok(())
}
fn has_cli_hidden(method: &MethodInfo) -> bool {
if has_server_hidden(method) {
return true;
}
for attr in &method.method.attrs {
if attr.path().is_ident("cli") {
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("hidden") {
found = true;
}
if meta.input.peek(syn::Token![=]) {
let _: proc_macro2::TokenStream = meta.value()?.parse()?;
}
Ok(())
});
if found {
return true;
}
}
}
false
}
fn get_display_with(method: &MethodInfo) -> Option<syn::Path> {
for attr in &method.method.attrs {
if attr.path().is_ident("cli") {
let mut found = None;
let result = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("display_with") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
found = Some(lit.parse::<syn::Path>()?);
Ok(())
} else {
if meta.input.peek(Token![=]) {
let _: syn::Expr = meta.value()?.parse()?;
}
Ok(())
}
});
if result.is_ok() {
return found;
}
}
}
None
}
fn cli_name(method: &MethodInfo) -> String {
method.wire_name_or(|n| n.to_kebab_case())
}
fn cli_param_name(param: &ParamInfo) -> String {
match ¶m.wire_name {
Some(w) => w.clone(),
None => param.name_str().to_kebab_case(),
}
}
fn clap_default_value(raw: &str) -> String {
if raw.len() >= 2 && raw.starts_with('"') && raw.ends_with('"') {
raw[1..raw.len() - 1].to_string()
} else {
raw.to_string()
}
}
fn build_group_order(
methods: &[&MethodInfo],
registry: &Option<server_less_parse::GroupRegistry>,
) -> syn::Result<Vec<String>> {
match registry {
Some(reg) => {
let used: std::collections::HashSet<String> = methods
.iter()
.filter_map(|m| m.group.clone())
.collect();
Ok(reg
.groups
.iter()
.filter(|(id, _)| used.contains(id))
.map(|(_, display)| display.clone())
.collect())
}
None => {
for m in methods {
resolve_method_group(m, registry)?;
}
Ok(vec![])
}
}
}
fn strip_cli_attrs(impl_block: &ItemImpl) -> ItemImpl {
let mut block = impl_block.clone();
block
.attrs
.retain(|attr| !attr.path().is_ident("server"));
for item in &mut block.items {
if let syn::ImplItem::Fn(method) = item {
method
.attrs
.retain(|attr| !attr.path().is_ident("cli") && !attr.path().is_ident("server"));
for input in &mut method.sig.inputs {
if let syn::FnArg::Typed(pat_type) = input {
pat_type.attrs.retain(|attr| !attr.path().is_ident("param"));
}
}
}
}
block
}
pub(crate) fn expand_cli(args: CliArgs, mut impl_block: ItemImpl) -> syn::Result<TokenStream2> {
crate::reject_generic_impl(&impl_block)?;
let app_meta = extract_app_meta(&mut impl_block.attrs);
let args = CliArgs {
name: args.name.or(app_meta.name),
description: args.description.or(app_meta.description),
version: args.version.or_else(|| app_meta.version.into_explicit()),
homepage: args.homepage.or(app_meta.homepage),
..args
};
let struct_name = get_impl_name(&impl_block)?;
let (impl_generics, _ty_generics, where_clause) = impl_block.generics.split_for_impl();
let self_ty = &impl_block.self_ty;
let methods = extract_methods(&impl_block)?;
let app_name = args
.name
.unwrap_or_else(|| struct_name.to_string().to_kebab_case());
let version_tokens = match args.version {
Some(ref v) => quote! { #v },
None => quote! { ::std::env!("CARGO_PKG_VERSION") },
};
let about = match (args.description.as_deref(), args.description_prefix) {
(Some(desc), Some(false)) => desc.to_string(),
(Some(desc), _) => format!("{app_name} - {desc}"),
(None, _) => String::new(),
};
let global_flags_with_help = args.global;
let global_flags: Vec<String> = global_flags_with_help
.iter()
.map(|(name, _)| name.clone())
.collect();
let globals_bound_assert = if global_flags.is_empty() {
quote! {}
} else {
quote! {
let _ = <Self as ::server_less::CliGlobals>::set_global_flag;
}
};
let has_defaults = args.defaults.is_some();
let defaults_fn_ident = args
.defaults
.as_ref()
.map(|name| syn::Ident::new(name, proc_macro2::Span::call_site()));
let no_sync = args.no_sync;
let no_async = args.no_async;
let meta = MetaFlags {
manual: args.manual.unwrap_or(true),
input_schema: args.input_schema.unwrap_or(true),
output_schema: args.output_schema.unwrap_or(true),
};
for m in &methods {
validate_server_attrs(m)?;
}
let partitioned = partition_methods(&methods, has_cli_skip);
check_reserved_flag_collisions(&partitioned, meta, &global_flags)?;
let group_registry = extract_groups(&impl_block)?;
let all_methods: Vec<&MethodInfo> = partitioned
.leaf
.iter()
.chain(partitioned.static_mounts.iter())
.chain(partitioned.slug_mounts.iter())
.copied()
.collect();
let group_order = build_group_order(&all_methods, &group_registry)?;
let has_groups = !group_order.is_empty();
let leaf_subcommands: Vec<_> = partitioned
.leaf
.iter()
.map(|m| {
let sub = generate_leaf_subcommand(
m,
has_defaults,
has_cli_hidden(m) || has_groups,
)?;
let cfg_attrs = &m.cfg_attrs;
Ok(quote! {
#(#cfg_attrs)*
let __cmd = __cmd.subcommand(#sub);
})
})
.collect::<syn::Result<Vec<_>>>()?;
let grouped_after_help = if has_groups {
#[allow(clippy::type_complexity)]
let mut sections: Vec<(Option<String>, Vec<(String, String)>)> = Vec::new();
let mut ungrouped = Vec::new();
for m in &partitioned.leaf {
if has_cli_hidden(m) {
continue;
}
if resolve_method_group(m, &group_registry)?.is_none() {
let name = cli_name(m);
let (about, _) = split_docs(&m.docs);
ungrouped.push((name, about));
}
}
for m in &partitioned.static_mounts {
if !has_cli_hidden(m) && resolve_method_group(m, &group_registry)?.is_none() {
let name = cli_name(m);
let (about, _) = split_docs(&m.docs);
ungrouped.push((name, about));
}
}
for m in &partitioned.slug_mounts {
if !has_cli_hidden(m) && resolve_method_group(m, &group_registry)?.is_none() {
let name = cli_name(m);
let (about, _) = split_docs(&m.docs);
ungrouped.push((name, about));
}
}
if !ungrouped.is_empty() {
sections.push((None, ungrouped));
}
for group in &group_order {
let mut entries = Vec::new();
for m in partitioned
.leaf
.iter()
.chain(partitioned.static_mounts.iter())
.chain(partitioned.slug_mounts.iter())
{
if has_cli_hidden(m) {
continue;
}
if resolve_method_group(m, &group_registry)?.as_deref() == Some(group.as_str()) {
let name = cli_name(m);
let (about, _) = split_docs(&m.docs);
entries.push((name, about));
}
}
if !entries.is_empty() {
sections.push((Some(group.clone()), entries));
}
}
let max_width = sections
.iter()
.flat_map(|(_, entries)| entries.iter())
.map(|(name, _)| name.len())
.max()
.unwrap_or(0);
let mut text = String::new();
for (heading, entries) in §ions {
match heading {
Some(h) => text.push_str(&format!("\x1b[1;4m{h}:\x1b[0m\n")),
None => text.push_str("\x1b[1;4mCommands:\x1b[0m\n"),
}
for (name, about) in entries {
if about.is_empty() {
text.push_str(&format!(" {name}\n"));
} else {
text.push_str(&format!(
" \x1b[1m{name:<width$}\x1b[0m {about}\n",
width = max_width
));
}
}
text.push('\n');
}
Some(quote! { .after_help(#text).subcommand_value_name("COMMAND") })
} else {
None
};
let static_mount_subcommands: Vec<_> = partitioned
.static_mounts
.iter()
.map(|m| generate_static_mount_subcommand(m, has_cli_hidden(m) || has_groups))
.collect::<syn::Result<Vec<_>>>()?;
let slug_mount_subcommands: Vec<_> = partitioned
.slug_mounts
.iter()
.map(|m| generate_slug_mount_subcommand(m, has_cli_hidden(m) || has_groups))
.collect::<syn::Result<Vec<_>>>()?;
let mut default_methods = partitioned.leaf.iter().filter(|m| has_cli_default(m));
let default_method = default_methods.next();
if let Some(second) = default_methods.next() {
let first_name = default_method
.map(|m| m.method.sig.ident.to_string())
.unwrap_or_default();
return Err(syn::Error::new_spanned(
&second.method.sig.ident,
format!(
"only one method may be marked as default; first default is '{first_name}'"
),
));
}
let default_parent_args: Vec<TokenStream2> = if let Some(dm) = default_method {
let (_, regular_params) = partition_context_params(&dm.params)?;
let mut pos_idx = 0usize;
regular_params
.iter()
.map(|p| {
let idx = if p.is_positional {
pos_idx += 1;
Some(pos_idx)
} else {
None
};
generate_arg(p, has_defaults, idx)
})
.collect()
} else {
vec![]
};
let default_none_arm: Option<TokenStream2> = if let Some(dm) = default_method {
Some(generate_leaf_match_arm(
dm,
&global_flags,
&defaults_fn_ident,
true,
false,
meta,
)?)
} else {
None
};
let async_default_none_arm: Option<TokenStream2> = if let Some(dm) = default_method {
Some(generate_leaf_match_arm(
dm,
&global_flags,
&defaults_fn_ident,
true,
true,
meta,
)?)
} else {
None
};
let leaf_match_arms: Vec<_> = partitioned
.leaf
.iter()
.map(|m| {
let arm = generate_leaf_match_arm(m, &global_flags, &defaults_fn_ident, false, false, meta)?;
let cfg_attrs = &m.cfg_attrs;
Ok(quote! {
#(#cfg_attrs)*
#arm
})
})
.collect::<syn::Result<Vec<_>>>()?;
let async_leaf_match_arms: Vec<_> = partitioned
.leaf
.iter()
.map(|m| {
let arm = generate_leaf_match_arm(m, &global_flags, &defaults_fn_ident, false, true, meta)?;
let cfg_attrs = &m.cfg_attrs;
Ok(quote! {
#(#cfg_attrs)*
#arm
})
})
.collect::<syn::Result<Vec<_>>>()?;
let static_mount_arms: Vec<_> = partitioned
.static_mounts
.iter()
.map(|m| generate_static_mount_arm(m))
.collect::<syn::Result<Vec<_>>>()?;
let async_static_mount_arms: Vec<_> = partitioned
.static_mounts
.iter()
.map(|m| generate_static_mount_arm_async(m))
.collect::<syn::Result<Vec<_>>>()?;
let slug_mount_arms: Vec<_> = partitioned
.slug_mounts
.iter()
.map(|m| generate_slug_mount_arm(m))
.collect::<syn::Result<Vec<_>>>()?;
let async_slug_mount_arms: Vec<_> = partitioned
.slug_mounts
.iter()
.map(|m| generate_slug_mount_arm_async(m))
.collect::<syn::Result<Vec<_>>>()?;
let manual_node_builders: Vec<TokenStream2> = {
let mut builders = Vec::new();
for m in &partitioned.leaf {
if has_cli_hidden(m) || has_cli_manual_false(m) {
continue;
}
let node = generate_leaf_manual_node(m)?;
let cfg_attrs = &m.cfg_attrs;
builders.push(quote! {
#(#cfg_attrs)*
#node
});
}
for m in &partitioned.static_mounts {
if has_cli_hidden(m) || has_cli_manual_false(m) {
continue;
}
let node = generate_static_mount_manual_node(m)?;
let cfg_attrs = &m.cfg_attrs;
builders.push(quote! {
#(#cfg_attrs)*
#node
});
}
for m in &partitioned.slug_mounts {
if has_cli_hidden(m) || has_cli_manual_false(m) {
continue;
}
let node = generate_slug_mount_manual_node(m)?;
let cfg_attrs = &m.cfg_attrs;
builders.push(quote! {
#(#cfg_attrs)*
#node
});
}
builders
};
let subcommand_docs: Vec<String> = partitioned
.leaf
.iter()
.filter(|m| !has_cli_hidden(m))
.map(|m| {
let name = cli_name(m);
match &m.docs {
Some(doc) => format!("- `{name}` — {doc}"),
None => format!("- `{name}`"),
}
})
.chain(
partitioned
.static_mounts
.iter()
.filter(|m| !has_cli_hidden(m))
.map(|m| {
let name = cli_name(m);
format!("- `{name}` (subcommand group)")
}),
)
.chain(
partitioned
.slug_mounts
.iter()
.filter(|m| !has_cli_hidden(m))
.map(|m| {
let name = cli_name(m);
format!("- `{name} <arg>` (subcommand group)")
}),
)
.collect();
let cli_command_doc = if subcommand_docs.is_empty() {
"Create a clap Command for this CLI.".to_string()
} else {
format!(
"Create a clap Command for this CLI.\n\n# Subcommands\n\n{}",
subcommand_docs.join("\n")
)
};
let clean_impl_block = if crate::is_protocol_impl_emitter(&impl_block, "cli") {
let stripped = strip_cli_attrs(&impl_block);
quote! { #stripped }
} else {
quote! {}
};
let global_flag_args: Vec<_> = global_flags_with_help
.iter()
.map(|(flag, help)| {
let kebab = flag.replace('_', "-");
let help_clause = help
.as_deref()
.map(|h| quote! { .help(#h) })
.unwrap_or_default();
quote! {
.arg(
::server_less::clap::Arg::new(#kebab)
.long(#kebab)
.action(::server_less::clap::ArgAction::SetTrue)
.global(true)
#help_clause
)
}
})
.collect();
let manual_nodes_method = quote! {
fn cli_manual_nodes(&self, __prefix: &str) -> Vec<::server_less::CliManualNode> {
let mut __nodes: Vec<::server_less::CliManualNode> = Vec::new();
#(#manual_node_builders)*
__nodes
}
};
let manual_dispatch_intercept = if meta.manual {
let emit = manual_emit_tokens(&syn::Ident::new(
"matches",
proc_macro2::Span::call_site(),
));
quote! {
if matches.get_flag("manual") && matches.subcommand().is_none() {
let __nodes = <Self as ::server_less::CliSubcommand>::cli_manual_nodes(self, "");
#emit
return Ok(());
}
}
} else {
quote! {}
};
let input_schema_flag = meta.input_schema.then(|| quote! {
.arg(
::server_less::clap::Arg::new("input-schema")
.long("input-schema")
.action(::server_less::clap::ArgAction::SetTrue)
.global(true)
.help("Print JSON Schema of the subcommand's input parameters and exit")
)
});
let output_schema_flag = meta.output_schema.then(|| quote! {
.arg(
::server_less::clap::Arg::new("output-schema")
.long("output-schema")
.action(::server_less::clap::ArgAction::SetTrue)
.global(true)
.help("Print JSON Schema of the subcommand's return type and exit")
)
});
let manual_flag = meta.manual.then(|| quote! {
.arg(
::server_less::clap::Arg::new("manual")
.long("manual")
.action(::server_less::clap::ArgAction::SetTrue)
.global(true)
.help("Emit the reference manual for the command subtree rooted here and exit")
)
});
let format_flags = quote! {
.arg(
::server_less::clap::Arg::new("jsonl")
.long("jsonl")
.action(::server_less::clap::ArgAction::SetTrue)
.global(true)
.help("Output one JSON object per line (for arrays)")
)
.arg(
::server_less::clap::Arg::new("json")
.long("json")
.action(::server_less::clap::ArgAction::SetTrue)
.global(true)
.help("Output machine-readable JSON")
)
.arg(
::server_less::clap::Arg::new("jq")
.long("jq")
.global(true)
.help("Filter output through jq expression")
)
#input_schema_flag
#output_schema_flag
#manual_flag
.arg(
::server_less::clap::Arg::new("params-json")
.long("params-json")
.global(true)
.help("Provide all parameters as a JSON object instead of individual flags")
)
};
#[cfg(feature = "completions")]
let completions_methods = quote! {
pub fn cli_completions<__W: ::std::io::Write>(
shell: ::server_less::clap_complete::Shell,
out: &mut __W,
) {
let mut __cmd = <Self as ::server_less::CliSubcommand>::cli_command();
let __bin = __cmd.get_name().to_string();
::server_less::clap_complete::generate(shell, &mut __cmd, __bin, out);
}
pub fn cli_manpage<__W: ::std::io::Write>(out: &mut __W) -> ::std::io::Result<()> {
let __cmd = <Self as ::server_less::CliSubcommand>::cli_command();
::server_less::clap_mangen::Man::new(__cmd).render(out)
}
};
#[cfg(not(feature = "completions"))]
let completions_methods = quote! {};
let sync_entrypoints = if !no_sync {
quote! {
pub fn cli_run(&self) -> ::std::result::Result<(), Box<dyn ::std::error::Error>> {
if ::server_less::tokio::runtime::Handle::try_current().is_ok() {
return Err(
"cli_run() cannot be called from within an async context (e.g. #[tokio::test] or #[tokio::main]). \\
Use cli_run_async() instead.".into()
);
}
let matches = Self::cli_command().get_matches();
<Self as ::server_less::CliSubcommand>::cli_dispatch(self, &matches)
}
pub fn cli_run_with<__CliI, __CliArg>(&self, args: __CliI) -> ::std::result::Result<(), Box<dyn ::std::error::Error>>
where
__CliI: IntoIterator<Item = __CliArg>,
__CliArg: Into<::std::ffi::OsString> + Clone,
{
if ::server_less::tokio::runtime::Handle::try_current().is_ok() {
return Err(
"cli_run_with() cannot be called from within an async context (e.g. #[tokio::test] or #[tokio::main]). \\
Use cli_run_with_async() instead.".into()
);
}
let matches = Self::cli_command().get_matches_from(args);
<Self as ::server_less::CliSubcommand>::cli_dispatch(self, &matches)
}
}
} else {
quote! {}
};
let async_entrypoint = if !no_async {
quote! {
pub async fn cli_run_async(&self) -> ::std::result::Result<(), Box<dyn ::std::error::Error>> {
let matches = Self::cli_command().get_matches();
<Self as ::server_less::CliSubcommand>::cli_dispatch_async(self, &matches).await
}
pub async fn cli_run_with_async<__CliI, __CliArg>(&self, args: __CliI) -> ::std::result::Result<(), Box<dyn ::std::error::Error>>
where
__CliI: IntoIterator<Item = __CliArg>,
__CliArg: Into<::std::ffi::OsString> + Clone,
{
let matches = Self::cli_command().get_matches_from(args);
<Self as ::server_less::CliSubcommand>::cli_dispatch_async(self, &matches).await
}
}
} else {
quote! {}
};
#[cfg(feature = "config")]
let (config_methods, config_subcommand_addition, config_dispatch_arm) =
if let Some(ref config_ty) = args.config_ty {
let cmd_name = args.config_cmd_name.as_deref().unwrap_or("config");
crate::config_cmd::generate_all(self_ty, config_ty, cmd_name, &app_name)
} else {
(quote! {}, quote! {}, quote! {})
};
#[cfg(not(feature = "config"))]
let (config_methods, config_subcommand_addition, config_dispatch_arm) =
(quote! {}, quote! {}, quote! {});
Ok(quote! {
#clean_impl_block
impl #impl_generics ::server_less::CliSubcommand for #self_ty #where_clause {
fn cli_command() -> ::server_less::clap::Command {
let mut __cmd = ::server_less::clap::Command::new(#app_name)
.version(#version_tokens)
.about(#about)
#(#global_flag_args)*
#format_flags
#grouped_after_help
#(.arg(#default_parent_args))*
#(.subcommand(#static_mount_subcommands))*
#(.subcommand(#slug_mount_subcommands))*
#config_subcommand_addition;
#(#leaf_subcommands)*
__cmd
}
#manual_nodes_method
fn cli_dispatch(&self, matches: &::server_less::clap::ArgMatches) -> ::std::result::Result<(), Box<dyn ::std::error::Error>> {
#globals_bound_assert
#manual_dispatch_intercept
match matches.subcommand() {
#(#leaf_match_arms)*
#(#static_mount_arms)*
#(#slug_mount_arms)*
#config_dispatch_arm
#default_none_arm
_ => {
Self::cli_command().print_help()?;
Ok(())
}
}
}
fn cli_dispatch_async<'__a>(
&'__a self,
matches: &'__a ::server_less::clap::ArgMatches,
) -> impl ::std::future::Future<Output = ::std::result::Result<(), Box<dyn ::std::error::Error>>> + '__a {
async move {
#globals_bound_assert
#manual_dispatch_intercept
match matches.subcommand() {
#(#async_leaf_match_arms)*
#(#async_static_mount_arms)*
#(#async_slug_mount_arms)*
#config_dispatch_arm
#async_default_none_arm
_ => {
Self::cli_command().print_help()?;
Ok(())
}
}
}
}
}
impl #impl_generics #self_ty #where_clause {
#[doc = #cli_command_doc]
pub fn cli_command() -> ::server_less::clap::Command {
<Self as ::server_less::CliSubcommand>::cli_command()
}
#sync_entrypoints
#async_entrypoint
#completions_methods
}
#config_methods
})
}
fn split_docs(docs: &Option<String>) -> (String, Option<String>) {
let full = match docs {
Some(s) => s.clone(),
None => return (String::new(), None),
};
if let Some(pos) = full.find("\n\n") {
let about = full[..pos].to_string();
let after = full[pos + 2..].to_string();
if after.is_empty() {
(about, None)
} else {
(about, Some(after))
}
} else {
(full, None)
}
}
fn generate_leaf_subcommand(
method: &MethodInfo,
has_defaults: bool,
hidden: bool,
) -> syn::Result<TokenStream2> {
let name = cli_name(method);
let (about, after_help) = split_docs(&method.docs);
let after_help_token = after_help.map(|h| quote! { .after_help(#h) });
let hide = hidden.then(|| quote! { .hide(true) });
let (_, regular_params) = partition_context_params(&method.params)?;
let mut pos_idx = 0usize;
let args: Vec<_> = regular_params
.iter()
.map(|p| {
let idx = if p.is_positional {
pos_idx += 1;
Some(pos_idx)
} else {
None
};
generate_arg(p, has_defaults, idx)
})
.collect();
Ok(quote! {
::server_less::clap::Command::new(#name)
.about(#about)
#after_help_token
#hide
#(.arg(#args))*
})
}
fn generate_static_mount_subcommand(
method: &MethodInfo,
hidden: bool,
) -> syn::Result<TokenStream2> {
let name = cli_name(method);
let (about, after_help) = split_docs(&method.docs);
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
let hide = hidden.then(|| quote! { __cmd = __cmd.hide(true); });
let after_help_set = after_help.map(|h| quote! { __cmd = __cmd.after_help(#h); });
Ok(quote! {
{
let mut __cmd = <#inner_ty as ::server_less::CliSubcommand>::cli_command()
.name(#name);
if !#about.is_empty() {
__cmd = __cmd.about(#about);
}
#after_help_set
#hide
__cmd
}
})
}
fn generate_slug_mount_subcommand(
method: &MethodInfo,
hidden: bool,
) -> syn::Result<TokenStream2> {
let name = cli_name(method);
let (about, after_help) = split_docs(&method.docs);
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
let hide = hidden.then(|| quote! { __cmd = __cmd.hide(true); });
let after_help_set = after_help.map(|h| quote! { __cmd = __cmd.after_help(#h); });
let (_, regular_params) = partition_context_params(&method.params)?;
let slug_args: Vec<_> = regular_params
.iter()
.enumerate()
.map(|(i, p)| {
let param_name = cli_param_name(p);
let idx = i + 1; quote! {
::server_less::clap::Arg::new(#param_name)
.required(true)
.index(#idx)
.help(concat!("The ", #param_name))
}
})
.collect();
Ok(quote! {
{
let mut __cmd = <#inner_ty as ::server_less::CliSubcommand>::cli_command()
.name(#name);
if !#about.is_empty() {
__cmd = __cmd.about(#about);
}
#after_help_set
#hide
#(__cmd = __cmd.arg(#slug_args);)*
__cmd
}
})
}
fn type_to_json_schema(ty: &Option<syn::Type>) -> TokenStream2 {
let Some(ty) = ty else {
return quote! { ::server_less::serde_json::json!({"type": "null"}) };
};
type_to_json_schema_ty(ty)
}
fn type_to_json_schema_ty(ty: &syn::Type) -> TokenStream2 {
use syn::{GenericArgument, PathArguments, Type};
match ty {
Type::Path(type_path) => {
let Some(segment) = type_path.path.segments.last() else {
return quote! { ::server_less::serde_json::json!({"type": "object"}) };
};
match segment.ident.to_string().as_str() {
"String" => quote! { ::server_less::serde_json::json!({"type": "string"}) },
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize"
| "usize" => quote! { ::server_less::serde_json::json!({"type": "integer"}) },
"f32" | "f64" => quote! { ::server_less::serde_json::json!({"type": "number"}) },
"bool" => quote! { ::server_less::serde_json::json!({"type": "boolean"}) },
"Vec" => {
if let PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(GenericArgument::Type(inner)) = args.args.first()
{
let items_schema = type_to_json_schema_ty(inner);
return quote! {
{
let __items = #items_schema;
::server_less::serde_json::json!({"type": "array", "items": __items})
}
};
}
quote! { ::server_less::serde_json::json!({"type": "array", "items": {}}) }
}
"HashMap" | "BTreeMap" | "IndexMap" => {
quote! { ::server_less::serde_json::json!({"type": "object", "additionalProperties": true}) }
}
"Option" => {
if let PathArguments::AngleBracketed(args) = &segment.arguments
&& let Some(GenericArgument::Type(inner)) = args.args.first()
{
return type_to_json_schema_ty(inner);
}
quote! { ::server_less::serde_json::json!({"type": "object"}) }
}
_ => quote! { ::server_less::serde_json::json!({"type": "object"}) },
}
}
Type::Reference(r) => {
if let Type::Path(tp) = r.elem.as_ref()
&& tp.path.is_ident("str")
{
quote! { ::server_less::serde_json::json!({"type": "string"}) }
} else {
type_to_json_schema_ty(&r.elem)
}
}
Type::Slice(_) => {
quote! { ::server_less::serde_json::json!({"type": "array", "items": {}}) }
}
_ => quote! { ::server_less::serde_json::json!({"type": "object"}) },
}
}
fn generate_arg(
param: &ParamInfo,
_has_defaults: bool,
positional_index: Option<usize>,
) -> TokenStream2 {
let name = cli_param_name(param);
let short = param.short_flag.map(|c| quote! { .short(#c) });
let default_clause = match ¶m.default_value {
Some(raw) if !param.is_bool && !param.is_vec => {
let dv = clap_default_value(raw);
quote! { .default_value(#dv) }
}
_ => quote! {},
};
#[cfg(feature = "jsonschema")]
let value_parser = if param.is_bool {
quote! {}
} else {
let inner: syn::Type = if param.is_vec {
param.vec_inner.clone().unwrap_or_else(|| param.ty.clone())
} else if param.is_optional {
extract_option_type(¶m.ty).unwrap_or_else(|| param.ty.clone())
} else {
param.ty.clone()
};
quote! { .value_parser(::server_less::SchemaValueParser::<#inner>::new()) }
};
#[cfg(not(feature = "jsonschema"))]
let value_parser = quote! {};
if param.is_bool {
let help = match ¶m.help_text {
Some(text) => quote! { .help(#text) },
None => quote! { .help(concat!("Enable ", #name)) },
};
quote! {
::server_less::clap::Arg::new(#name)
.long(#name)
#short
.action(::server_less::clap::ArgAction::SetTrue)
#help
}
} else if param.is_vec {
let help = match ¶m.help_text {
Some(text) => quote! { .help(#text) },
None => quote! { .help(concat!("Repeatable: ", #name)) },
};
quote! {
::server_less::clap::Arg::new(#name)
.long(#name)
#short
.action(::server_less::clap::ArgAction::Append)
.value_delimiter(',')
.required(false)
#value_parser
#help
}
} else if param.is_positional {
let idx = positional_index.unwrap_or(1);
let help = match ¶m.help_text {
Some(text) => quote! { .help(#text) },
None => quote! { .help(concat!("The ", #name)) },
};
quote! {
::server_less::clap::Arg::new(#name)
.required(false)
.index(#idx)
#value_parser
#default_clause
#help
}
} else if param.is_optional {
let help = match ¶m.help_text {
Some(text) => quote! { .help(#text) },
None => quote! { .help(concat!("Optional: ", #name)) },
};
quote! {
::server_less::clap::Arg::new(#name)
.long(#name)
#short
.required(false)
#value_parser
#default_clause
#help
}
} else {
let help = match ¶m.help_text {
Some(text) => quote! { .help(#text) },
None => quote! { .help(concat!("Required: ", #name)) },
};
quote! {
::server_less::clap::Arg::new(#name)
.long(#name)
#short
.required(false)
#value_parser
#default_clause
#help
}
}
}
fn generate_leaf_match_arm(
method: &MethodInfo,
global_flags: &[String],
defaults_fn_ident: &Option<syn::Ident>,
none_arm: bool,
for_async: bool,
meta: MetaFlags,
) -> syn::Result<TokenStream2> {
let subcommand_name = cli_name(method);
let method_name = &method.name;
let (context_param, regular_params) = partition_context_params(&method.params)?;
let input_schema = {
let mut props = Vec::new();
let mut required = Vec::new();
for p in ®ular_params {
let name_str = p.name_str();
let schema = type_to_json_schema(&Some(p.ty.clone()));
props.push(quote! {
__props.insert(#name_str.to_string(), #schema);
});
if !p.is_optional && !p.is_bool {
required.push(quote! { #name_str });
}
}
quote! {
let mut __props = ::server_less::serde_json::Map::new();
#(#props)*
let __schema = ::server_less::serde_json::json!({
"type": "object",
"properties": ::server_less::serde_json::Value::Object(__props),
"required": [#(#required),*],
});
println!("{}", ::server_less::serde_json::to_string_pretty(&__schema)?);
return Ok(());
}
};
let output_ty = if method.return_info.is_result {
&method.return_info.ok_type
} else if method.return_info.is_option {
&method.return_info.some_type
} else if method.return_info.is_unit {
&None
} else {
&method.return_info.ty
};
#[cfg(feature = "jsonschema")]
let output_schema = if let Some(ty) = output_ty {
quote! {
let __schema = ::server_less::cli_schema_for::<#ty>();
println!("{}", ::server_less::serde_json::to_string_pretty(&__schema)?);
return Ok(());
}
} else {
quote! {
println!("{{\"type\": \"null\"}}");
return Ok(());
}
};
#[cfg(not(feature = "jsonschema"))]
let output_schema = {
let output_schema_expr = type_to_json_schema(output_ty);
quote! {
let __schema = #output_schema_expr;
println!("{}", ::server_less::serde_json::to_string_pretty(&__schema)?);
return Ok(());
}
};
let mut arg_extractions = Vec::new();
let mut arg_names = Vec::new();
if context_param.is_some() {
let (_extraction, call) = generate_cli_context_extraction();
arg_extractions.push(quote! {
let __ctx = #call;
});
arg_names.push(quote! { __ctx });
}
for p in ®ular_params {
let name = &p.name;
let name_str = cli_param_name(p);
if p.is_bool {
arg_extractions.push(quote! {
let #name: bool = sub_matches.get_flag(#name_str);
});
} else if p.is_vec {
#[cfg(feature = "jsonschema")]
{
let inner = p.vec_inner.as_ref().unwrap_or(&p.ty);
arg_extractions.push(quote! {
let #name = sub_matches
.get_many::<#inner>(#name_str)
.map(|vs| vs.cloned().collect())
.unwrap_or_default();
});
}
#[cfg(not(feature = "jsonschema"))]
arg_extractions.push(quote! {
let #name: Vec<String> = sub_matches
.get_many::<String>(#name_str)
.map(|vs| vs.cloned().collect())
.unwrap_or_default();
});
} else if p.is_optional {
let ty = &p.ty;
#[cfg(feature = "jsonschema")]
{
let inner = extract_option_type(&p.ty).unwrap_or_else(|| p.ty.clone());
arg_extractions.push(quote! {
let #name: #ty = sub_matches
.get_one::<#inner>(#name_str)
.cloned();
});
}
#[cfg(not(feature = "jsonschema"))]
arg_extractions.push(quote! {
let #name: #ty = sub_matches
.get_one::<String>(#name_str)
.and_then(|s| s.parse().ok());
});
} else if let Some(defaults_fn) = defaults_fn_ident {
let ty = &p.ty;
#[cfg(feature = "jsonschema")]
arg_extractions.push(quote! {
let #name: #ty = if let Some(__val) = sub_matches.get_one::<#ty>(#name_str) {
__val.clone()
} else if let Some(__default) = self.#defaults_fn(#name_str) {
__default.parse()?
} else {
return Err(format!("Missing required argument: {}", #name_str).into());
};
});
#[cfg(not(feature = "jsonschema"))]
arg_extractions.push(quote! {
let #name: #ty = if let Some(__val) = sub_matches.get_one::<String>(#name_str) {
__val.parse()?
} else if let Some(__default) = self.#defaults_fn(#name_str) {
__default.parse()?
} else {
return Err(format!("Missing required argument: {}", #name_str).into());
};
});
} else {
let ty = &p.ty;
#[cfg(feature = "jsonschema")]
arg_extractions.push(quote! {
let #name: #ty = sub_matches
.get_one::<#ty>(#name_str)
.cloned()
.ok_or_else(|| format!("Missing required argument: {}", #name_str))?;
});
#[cfg(not(feature = "jsonschema"))]
arg_extractions.push(quote! {
let #name: #ty = sub_matches
.get_one::<String>(#name_str)
.map(|s| s.parse())
.transpose()?
.ok_or_else(|| format!("Missing required argument: {}", #name_str))?;
});
}
arg_names.push(quote! { #name });
}
let mut json_extractions = Vec::new();
let mut json_arg_names = Vec::new();
if context_param.is_some() {
let (_extraction, call) = generate_cli_context_extraction();
json_extractions.push(quote! {
let __ctx = #call;
});
json_arg_names.push(quote! { __ctx });
}
for p in ®ular_params {
let name = &p.name;
let name_str = p.name_str();
let ty = &p.ty;
if p.is_bool {
json_extractions.push(quote! {
let #name: bool = __json_obj.get(#name_str)
.and_then(|v| v.as_bool())
.unwrap_or(false);
});
} else if p.is_optional {
json_extractions.push(quote! {
let #name: #ty = __json_obj.get(#name_str)
.and_then(|v| ::server_less::serde_json::from_value(v.clone()).ok());
});
} else if p.is_vec {
let elem_ty = p.vec_inner.as_ref().unwrap_or(ty);
json_extractions.push(quote! {
let #name: Vec<#elem_ty> = __json_obj.get(#name_str)
.map(|v| ::server_less::serde_json::from_value(v.clone()))
.transpose()
.map_err(|e| format!("Invalid value for '{}': {}", #name_str, e))?
.unwrap_or_default();
});
} else {
json_extractions.push(quote! {
let #name: #ty = __json_obj.get(#name_str)
.ok_or_else(|| format!("Missing required field '{}' in --params-json", #name_str))
.and_then(|v| ::server_less::serde_json::from_value(v.clone())
.map_err(|e| format!("Invalid value for '{}': {}", #name_str, e)))?;
});
}
json_arg_names.push(quote! { #name });
}
let gen_call = |names: &[TokenStream2]| -> TokenStream2 {
if method.return_info.is_unit {
if method.is_async {
if for_async {
quote! { self.#method_name(#(#names),*).await; }
} else {
quote! {
::server_less::tokio::runtime::Runtime::new()?
.block_on(self.#method_name(#(#names),*));
}
}
} else {
quote! {
self.#method_name(#(#names),*);
}
}
} else if method.is_async {
if for_async {
quote! { let result = self.#method_name(#(#names),*).await; }
} else {
quote! {
let result = ::server_less::tokio::runtime::Runtime::new()?
.block_on(self.#method_name(#(#names),*));
}
}
} else {
quote! {
let result = self.#method_name(#(#names),*);
}
}
};
let call = gen_call(&arg_names);
let json_call = gen_call(&json_arg_names);
let format_extraction = quote! {
let __jsonl = sub_matches.get_flag("jsonl");
let __json = sub_matches.get_flag("json");
let __jq: Option<&String> = sub_matches.get_one::<String>("jq");
};
let display_with = get_display_with(method);
let inner_ty = if method.return_info.is_result {
method.return_info.ok_type.as_ref()
} else if method.return_info.is_option {
method.return_info.some_type.as_ref()
} else {
method.return_info.ty.as_ref()
};
let gen_value_display = |value_ident: &syn::Ident| -> TokenStream2 {
let text_display = if let Some(ref display_fn) = display_with {
quote! {
let __display = self.#display_fn(&#value_ident);
println!("{}", __display);
}
} else if let Some(ty) = inner_ty {
if is_unit_type(ty) {
quote! { println!("Done"); }
} else if extract_vec_type(ty).is_some() {
quote! {
for __item in &#value_ident {
println!("{}", __item);
}
}
} else if extract_map_type(ty).is_some() {
quote! {
for (__k, __v) in &#value_ident {
println!("{}: {}", __k, __v);
}
}
} else {
quote! { println!("{}", #value_ident); }
}
} else {
quote! { println!("{}", #value_ident); }
};
quote! {
if __json || __jsonl || __jq.is_some() {
let __formatted = ::server_less::cli_format_output(
::server_less::serde_json::to_value(&#value_ident)?,
__jsonl, __json, __jq.map(|s| s.as_str()),
)?;
println!("{}", __formatted);
} else {
#text_display
}
}
};
let value_ident = syn::Ident::new("value", proc_macro2::Span::call_site());
let output = if method.return_info.is_unit {
quote! { println!("Done"); }
} else if method.return_info.is_result {
let display_code = gen_value_display(&value_ident);
quote! {
match result {
Ok(value) => {
#display_code
}
Err(err) => {
let __err_msg = ::std::format!("{}", err);
if __json || __jsonl || __jq.is_some() {
let __err_val = ::server_less::serde_json::json!({ "error": __err_msg });
match ::server_less::cli_format_output(
__err_val, __jsonl, __json, __jq.map(|s| s.as_str()),
) {
Ok(__formatted) => println!("{}", __formatted),
Err(_) => eprintln!("{}", __err_msg),
}
} else {
eprintln!("{}", __err_msg);
}
::std::process::exit(1);
}
}
}
} else if method.return_info.is_option {
let display_code = gen_value_display(&value_ident);
quote! {
match result {
Some(value) => {
#display_code
}
None => {
if __json || __jsonl || __jq.is_some() {
let __formatted = ::server_less::cli_format_output(
::server_less::serde_json::Value::Null,
__jsonl, __json, __jq.map(|s| s.as_str()),
)?;
println!("{}", __formatted);
} else {
eprintln!("Not found");
::std::process::exit(1);
}
}
}
}
} else if method.return_info.is_iterator {
quote! {
if __json || __jq.is_some() {
let __collected: Vec<_> = result.collect();
let __formatted = ::server_less::cli_format_output(
::server_less::serde_json::to_value(&__collected)?,
false, __json, __jq.map(|s| s.as_str()),
)?;
println!("{}", __formatted);
} else {
for __item in result {
println!("{}", ::server_less::serde_json::to_string(&__item)?);
}
}
}
} else {
let result_ident = syn::Ident::new("result", proc_macro2::Span::call_site());
let display_code = gen_value_display(&result_ident);
quote! {
#display_code
}
};
let leaf_manual_node = generate_leaf_manual_node(method)?;
let manual_emit = manual_emit_tokens(&syn::Ident::new(
"sub_matches",
proc_macro2::Span::call_site(),
));
let manual_arm = meta.manual.then(|| quote! {
if sub_matches.get_flag("manual") {
let mut __nodes: Vec<::server_less::CliManualNode> = Vec::new();
let __prefix: &str = "";
#leaf_manual_node
#manual_emit
return Ok(());
}
});
let input_schema_arm = meta.input_schema.then(|| quote! {
if sub_matches.get_flag("input-schema") {
#input_schema
}
});
let output_schema_arm = meta.output_schema.then(|| quote! {
if sub_matches.get_flag("output-schema") {
#output_schema
}
});
let global_delivery: Vec<TokenStream2> = global_flags
.iter()
.map(|flag| {
let kebab = flag.replace('_', "-");
quote! {
<Self as ::server_less::CliGlobals>::set_global_flag(
self, #kebab, sub_matches.get_flag(#kebab),
);
}
})
.collect();
let arm_body = quote! {
#manual_arm
#input_schema_arm
#output_schema_arm
#(#global_delivery)*
if let Some(__params_json_str) = sub_matches.get_one::<String>("params-json") {
let __json_obj: ::server_less::serde_json::Value = ::server_less::serde_json::from_str(__params_json_str)
.map_err(|e| format!("Invalid JSON in --params-json: {}", e))?;
let __json_obj = __json_obj.as_object()
.ok_or_else(|| "Expected a JSON object for --params-json".to_string())?;
#(#json_extractions)*
#json_call
#format_extraction
#output
} else {
#(#arg_extractions)*
#call
#format_extraction
#output
}
Ok(())
};
Ok(if none_arm {
quote! {
None => {
let sub_matches = matches;
#arm_body
}
}
} else {
quote! {
Some((#subcommand_name, sub_matches)) => {
#arm_body
}
}
})
}
fn manual_emit_tokens(matches_ident: &syn::Ident) -> TokenStream2 {
quote! {
let __json = #matches_ident.get_flag("json");
let __jsonl = #matches_ident.get_flag("jsonl");
let __jq: Option<&String> = #matches_ident.get_one::<String>("jq");
if __json || __jsonl || __jq.is_some() {
let __doc = ::server_less::cli_manual_to_json(&__nodes);
let __formatted = ::server_less::cli_format_output(
__doc, __jsonl, __json, __jq.map(|s| s.as_str()),
)?;
println!("{}", __formatted);
} else {
print!("{}", ::server_less::cli_manual_to_text(&__nodes));
}
}
}
fn leaf_input_schema_expr(method: &MethodInfo) -> syn::Result<TokenStream2> {
let (_, regular_params) = partition_context_params(&method.params)?;
let mut props = Vec::new();
let mut required = Vec::new();
for p in ®ular_params {
let name_str = p.name_str();
let schema = type_to_json_schema(&Some(p.ty.clone()));
props.push(quote! {
__props.insert(#name_str.to_string(), #schema);
});
if !p.is_optional && !p.is_bool {
required.push(quote! { #name_str });
}
}
Ok(quote! {
{
let mut __props = ::server_less::serde_json::Map::new();
#(#props)*
::server_less::serde_json::json!({
"type": "object",
"properties": ::server_less::serde_json::Value::Object(__props),
"required": [#(#required),*],
})
}
})
}
fn leaf_output_schema_expr(method: &MethodInfo) -> TokenStream2 {
let output_ty = if method.return_info.is_result {
&method.return_info.ok_type
} else if method.return_info.is_option {
&method.return_info.some_type
} else if method.return_info.is_unit {
&None
} else {
&method.return_info.ty
};
#[cfg(feature = "jsonschema")]
{
if let Some(ty) = output_ty {
quote! { ::server_less::cli_schema_for::<#ty>() }
} else {
quote! { ::server_less::serde_json::json!({"type": "null"}) }
}
}
#[cfg(not(feature = "jsonschema"))]
{
type_to_json_schema(output_ty)
}
}
fn generate_leaf_manual_node(method: &MethodInfo) -> syn::Result<TokenStream2> {
let name = cli_name(method);
let input_schema = leaf_input_schema_expr(method)?;
let output_schema = leaf_output_schema_expr(method);
let (about, _) = split_docs(&method.docs);
let description = if about.is_empty() {
quote! { ::std::option::Option::None }
} else {
quote! { ::std::option::Option::Some(#about.to_string()) }
};
Ok(quote! {
{
let __path = if __prefix.is_empty() {
#name.to_string()
} else {
format!("{} {}", __prefix, #name)
};
__nodes.push(::server_less::CliManualNode {
path: __path,
description: #description,
input_schema: #input_schema,
output_schema: #output_schema,
});
}
})
}
fn generate_static_mount_manual_node(method: &MethodInfo) -> syn::Result<TokenStream2> {
let name = cli_name(method);
let method_name = &method.name;
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
Ok(quote! {
{
let __child_prefix = if __prefix.is_empty() {
#name.to_string()
} else {
format!("{} {}", __prefix, #name)
};
let __delegate = self.#method_name();
__nodes.extend(
<#inner_ty as ::server_less::CliSubcommand>::cli_manual_nodes(__delegate, &__child_prefix)
);
}
})
}
fn generate_slug_mount_manual_node(method: &MethodInfo) -> syn::Result<TokenStream2> {
let name = cli_name(method);
let (_, regular_params) = partition_context_params(&method.params)?;
let slug_names: Vec<String> = regular_params
.iter()
.map(|p| format!("<{}>", cli_param_name(p)))
.collect();
let slug_suffix = if slug_names.is_empty() {
String::new()
} else {
format!(" {}", slug_names.join(" "))
};
let (about, _) = split_docs(&method.docs);
let desc_base = if about.is_empty() {
"command group, selected by an <id> argument; run with an id value and --manual to see its subcommands"
.to_string()
} else {
format!(
"{about} — command group, selected by an <id> argument; run with an id value and --manual to see its subcommands"
)
};
let path_suffix = slug_suffix.clone();
Ok(quote! {
{
let __path = if __prefix.is_empty() {
format!("{}{}", #name, #path_suffix)
} else {
format!("{} {}{}", __prefix, #name, #path_suffix)
};
__nodes.push(::server_less::CliManualNode {
path: __path,
description: ::std::option::Option::Some(#desc_base.to_string()),
input_schema: ::server_less::serde_json::json!({"type": "object"}),
output_schema: ::server_less::serde_json::json!({"type": "object"}),
});
}
})
}
fn generate_static_mount_arm(method: &MethodInfo) -> syn::Result<TokenStream2> {
let subcommand_name = cli_name(method);
let method_name = &method.name;
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
Ok(quote! {
Some((#subcommand_name, sub_matches)) => {
let __delegate = self.#method_name();
<#inner_ty as ::server_less::CliSubcommand>::cli_dispatch(__delegate, sub_matches)
}
})
}
fn generate_static_mount_arm_async(method: &MethodInfo) -> syn::Result<TokenStream2> {
let subcommand_name = cli_name(method);
let method_name = &method.name;
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
Ok(quote! {
Some((#subcommand_name, sub_matches)) => {
let __delegate = self.#method_name();
<#inner_ty as ::server_less::CliSubcommand>::cli_dispatch_async(__delegate, sub_matches).await
}
})
}
fn generate_slug_mount_arm(method: &MethodInfo) -> syn::Result<TokenStream2> {
let subcommand_name = cli_name(method);
let method_name = &method.name;
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
let (_, regular_params) = partition_context_params(&method.params)?;
let mut slug_extractions = Vec::new();
let mut slug_names = Vec::new();
for p in regular_params {
let name = &p.name;
let name_str = cli_param_name(p);
let ty = &p.ty;
slug_extractions.push(quote! {
let #name: #ty = sub_matches
.get_one::<String>(#name_str)
.map(|s| s.parse())
.transpose()?
.ok_or_else(|| format!("Missing required argument: {}", #name_str))?;
});
slug_names.push(quote! { #name });
}
Ok(quote! {
Some((#subcommand_name, sub_matches)) => {
#(#slug_extractions)*
let __delegate = self.#method_name(#(#slug_names),*);
<#inner_ty as ::server_less::CliSubcommand>::cli_dispatch(__delegate, sub_matches)
}
})
}
fn generate_slug_mount_arm_async(
method: &MethodInfo,
) -> syn::Result<TokenStream2> {
let subcommand_name = cli_name(method);
let method_name = &method.name;
let inner_ty = method.return_info.reference_inner.as_ref().ok_or_else(|| {
syn::Error::new_spanned(
&method.method.sig,
"BUG: mount method must have a reference return type (&T)",
)
})?;
let (_, regular_params) = partition_context_params(&method.params)?;
let mut slug_extractions = Vec::new();
let mut slug_names = Vec::new();
for p in regular_params {
let name = &p.name;
let name_str = cli_param_name(p);
let ty = &p.ty;
slug_extractions.push(quote! {
let #name: #ty = sub_matches
.get_one::<String>(#name_str)
.map(|s| s.parse())
.transpose()?
.ok_or_else(|| format!("Missing required argument: {}", #name_str))?;
});
slug_names.push(quote! { #name });
}
Ok(quote! {
Some((#subcommand_name, sub_matches)) => {
#(#slug_extractions)*
let __delegate = self.#method_name(#(#slug_names),*);
<#inner_ty as ::server_less::CliSubcommand>::cli_dispatch_async(__delegate, sub_matches).await
}
})
}