1use proc_macro::TokenStream;
71use proc_macro2::{Span, TokenStream as TokenStream2};
72use quote::{quote, quote_spanned};
73use syn::{
74 parse_macro_input, Attribute, Data, DeriveInput, Expr, ExprLit, Fields, GenericArgument,
75 Ident, Lit, PathArguments, Type, TypePath,
76};
77
78const RESERVED_LONGS: &[&str] = &["help", "version", "quiet", "env"];
81const RESERVED_SHORTS: &[char] = &['h', 'V', 'q', 'v', 'e'];
82
83#[proc_macro_derive(FdlArgs, attributes(option, arg))]
90pub fn derive_fdl_args(input: TokenStream) -> TokenStream {
91 let input = parse_macro_input!(input as DeriveInput);
92 match impl_derive(input) {
93 Ok(ts) => ts,
94 Err(e) => e.to_compile_error().into(),
95 }
96}
97
98fn impl_derive(input: DeriveInput) -> syn::Result<TokenStream> {
99 let ident = &input.ident;
100 let description = extract_doc(&input.attrs);
101
102 let fields = match &input.data {
103 Data::Struct(s) => match &s.fields {
104 Fields::Named(n) => &n.named,
105 _ => {
106 return Err(syn::Error::new_spanned(
107 ident,
108 "FdlArgs requires a struct with named fields",
109 ));
110 }
111 },
112 _ => {
113 return Err(syn::Error::new_spanned(
114 ident,
115 "FdlArgs requires a struct",
116 ));
117 }
118 };
119
120 let mut parsed: Vec<FieldSpec> = Vec::new();
121 for f in fields {
122 parsed.push(parse_field(f)?);
123 }
124
125 validate_collisions(&parsed)?;
126
127 let spec_build = build_spec_expr(&parsed);
128 let schema_build = build_schema_expr(&parsed, description.as_deref());
129 let extract = build_extractor(ident, &parsed)?;
130 let render_help = build_help_expr(&parsed, description.as_deref(), &ident.to_string());
131 let env_injection = build_env_injection(&parsed);
132
133 let expanded = quote! {
134 impl ::flodl_cli::FdlArgsTrait for #ident {
135 fn try_parse_from(args: &[::std::string::String])
136 -> ::std::result::Result<Self, ::std::string::String>
137 {
138 let spec = #spec_build;
139 #env_injection
140 let parsed = ::flodl_cli::args::parser::parse(&spec, args)?;
141 #extract
142 }
143
144 fn schema() -> ::flodl_cli::Schema {
145 #schema_build
146 }
147
148 fn render_help() -> ::std::string::String {
149 #render_help
150 }
151 }
152 };
153 Ok(expanded.into())
154}
155
156#[derive(Clone)]
159enum FieldKind {
160 Option,
161 Arg,
162}
163
164#[derive(Clone)]
165enum TypeShape {
166 Bool,
168 Scalar,
170 Opt,
172 List,
174}
175
176#[derive(Clone)]
177struct FieldSpec {
178 ident: Ident,
179 kind: FieldKind,
180 shape: TypeShape,
181 inner_ty: Type,
183 description: Option<String>,
184 short: Option<char>,
186 default: Option<String>,
187 choices: Option<Vec<String>>,
188 env: Option<String>,
189 completer: Option<String>,
190 variadic: bool,
191 span: Span,
192}
193
194fn parse_field(f: &syn::Field) -> syn::Result<FieldSpec> {
195 let ident = f.ident.clone().ok_or_else(|| {
196 syn::Error::new_spanned(f, "FdlArgs requires named fields")
197 })?;
198 let description = extract_doc(&f.attrs);
199 let (shape, inner_ty) = classify_type(&f.ty);
200
201 let mut kind: Option<FieldKind> = None;
205 let mut short: Option<char> = None;
206 let mut default: Option<String> = None;
207 let mut choices: Option<Vec<String>> = None;
208 let mut env: Option<String> = None;
209 let mut completer: Option<String> = None;
210 let mut variadic = false;
211
212 for attr in &f.attrs {
213 if attr.path().is_ident("option") {
214 if kind.is_some() {
215 return Err(syn::Error::new_spanned(
216 attr,
217 "field cannot have both #[option] and #[arg]",
218 ));
219 }
220 kind = Some(FieldKind::Option);
221 parse_option_attr(attr, &mut short, &mut default, &mut choices, &mut env, &mut completer)?;
222 } else if attr.path().is_ident("arg") {
223 if kind.is_some() {
224 return Err(syn::Error::new_spanned(
225 attr,
226 "field cannot have both #[option] and #[arg]",
227 ));
228 }
229 kind = Some(FieldKind::Arg);
230 parse_arg_attr(attr, &mut default, &mut choices, &mut variadic, &mut completer)?;
231 }
232 }
233
234 let kind = kind.ok_or_else(|| {
235 syn::Error::new_spanned(
236 &ident,
237 "field must carry either #[option] or #[arg]",
238 )
239 })?;
240
241 match kind {
243 FieldKind::Option => {
244 if matches!(shape, TypeShape::Bool) && default.is_some() {
245 return Err(syn::Error::new_spanned(
246 &f.ty,
247 "#[option(default = ...)] is meaningless on a bool flag (absent=false, present=true)",
248 ));
249 }
250 if matches!(shape, TypeShape::Bool) && env.is_some() {
251 return Err(syn::Error::new_spanned(
252 &f.ty,
253 "#[option(env = ...)] is not supported on bare `bool` (truthy/falsy string semantics are ambiguous) — use `Option<bool>` if you need env fallback",
254 ));
255 }
256 if matches!(shape, TypeShape::Scalar) && default.is_none() && !matches!(shape, TypeShape::Bool) {
257 return Err(syn::Error::new_spanned(
258 &f.ty,
259 "#[option] on a non-Option, non-bool type requires `default = \"...\"` (the field must always have a value)",
260 ));
261 }
262 if variadic {
263 return Err(syn::Error::new_spanned(
264 &ident,
265 "`variadic` only applies to #[arg], not #[option]",
266 ));
267 }
268 }
269 FieldKind::Arg => {
270 if matches!(shape, TypeShape::Bool) {
271 return Err(syn::Error::new_spanned(
272 &f.ty,
273 "positional #[arg] cannot be a bool (positionals always carry a value)",
274 ));
275 }
276 if short.is_some() {
277 return Err(syn::Error::new_spanned(
278 &ident,
279 "`short` cannot be used on #[arg] (positionals have no short form)",
280 ));
281 }
282 if variadic && !matches!(shape, TypeShape::List) {
283 return Err(syn::Error::new_spanned(
284 &f.ty,
285 "#[arg(variadic)] requires a Vec<T> field",
286 ));
287 }
288 }
289 }
290
291 Ok(FieldSpec {
292 ident,
293 kind,
294 shape,
295 inner_ty,
296 description,
297 short,
298 default,
299 choices,
300 env,
301 completer,
302 variadic,
303 span: f.span(),
304 })
305}
306
307fn parse_option_attr(
310 attr: &Attribute,
311 short: &mut Option<char>,
312 default: &mut Option<String>,
313 choices: &mut Option<Vec<String>>,
314 env: &mut Option<String>,
315 completer: &mut Option<String>,
316) -> syn::Result<()> {
317 if matches!(attr.meta, syn::Meta::Path(_)) {
318 return Ok(()); }
320 attr.parse_nested_meta(|meta| {
321 let key = meta
322 .path
323 .get_ident()
324 .ok_or_else(|| meta.error("expected identifier key in #[option]"))?;
325 match key.to_string().as_str() {
326 "short" => {
327 let v: syn::LitChar = meta.value()?.parse()?;
328 *short = Some(v.value());
329 }
330 "default" => {
331 let v: syn::LitStr = meta.value()?.parse()?;
332 *default = Some(v.value());
333 }
334 "choices" => {
335 *choices = Some(parse_choices(&meta)?);
336 }
337 "env" => {
338 let v: syn::LitStr = meta.value()?.parse()?;
339 *env = Some(v.value());
340 }
341 "completer" => {
342 let v: syn::LitStr = meta.value()?.parse()?;
343 *completer = Some(v.value());
344 }
345 other => {
346 return Err(meta.error(format!(
347 "unknown #[option] attribute `{other}` (valid: short, default, choices, env, completer)"
348 )));
349 }
350 }
351 Ok(())
352 })
353}
354
355fn parse_arg_attr(
356 attr: &Attribute,
357 default: &mut Option<String>,
358 choices: &mut Option<Vec<String>>,
359 variadic: &mut bool,
360 completer: &mut Option<String>,
361) -> syn::Result<()> {
362 if matches!(attr.meta, syn::Meta::Path(_)) {
363 return Ok(());
364 }
365 attr.parse_nested_meta(|meta| {
366 let key = meta
367 .path
368 .get_ident()
369 .ok_or_else(|| meta.error("expected identifier key in #[arg]"))?;
370 match key.to_string().as_str() {
371 "default" => {
372 let v: syn::LitStr = meta.value()?.parse()?;
373 *default = Some(v.value());
374 }
375 "choices" => {
376 *choices = Some(parse_choices(&meta)?);
377 }
378 "variadic" => {
379 *variadic = true;
381 if meta.input.peek(syn::Token![=]) {
382 let v: syn::LitBool = meta.value()?.parse()?;
383 *variadic = v.value();
384 }
385 }
386 "completer" => {
387 let v: syn::LitStr = meta.value()?.parse()?;
388 *completer = Some(v.value());
389 }
390 other => {
391 return Err(meta.error(format!(
392 "unknown #[arg] attribute `{other}` (valid: default, choices, variadic, completer)"
393 )));
394 }
395 }
396 Ok(())
397 })
398}
399
400fn parse_choices(meta: &syn::meta::ParseNestedMeta) -> syn::Result<Vec<String>> {
401 let expr: Expr = meta.value()?.parse()?;
403 let arr = match expr {
404 Expr::Reference(r) => *r.expr,
405 e => e,
406 };
407 match arr {
408 Expr::Array(arr) => {
409 let mut out = Vec::with_capacity(arr.elems.len());
410 for e in arr.elems {
411 if let Expr::Lit(ExprLit {
412 lit: Lit::Str(s), ..
413 }) = e
414 {
415 out.push(s.value());
416 } else {
417 return Err(syn::Error::new_spanned(
418 e,
419 "choices must be string literals",
420 ));
421 }
422 }
423 Ok(out)
424 }
425 other => Err(syn::Error::new_spanned(
426 other,
427 "choices must be an array literal, e.g. `&[\"a\", \"b\"]`",
428 )),
429 }
430}
431
432fn classify_type(ty: &Type) -> (TypeShape, Type) {
435 if let Type::Path(TypePath { path, .. }) = ty {
436 if let Some(seg) = path.segments.last() {
437 let name = seg.ident.to_string();
438 if name == "bool" {
439 return (TypeShape::Bool, ty.clone());
440 }
441 if name == "Option" {
442 if let Some(inner) = first_generic(&seg.arguments) {
443 return (TypeShape::Opt, inner);
444 }
445 }
446 if name == "Vec" {
447 if let Some(inner) = first_generic(&seg.arguments) {
448 return (TypeShape::List, inner);
449 }
450 }
451 }
452 }
453 (TypeShape::Scalar, ty.clone())
454}
455
456fn first_generic(args: &PathArguments) -> Option<Type> {
457 if let PathArguments::AngleBracketed(a) = args {
458 for arg in &a.args {
459 if let GenericArgument::Type(t) = arg {
460 return Some(t.clone());
461 }
462 }
463 }
464 None
465}
466
467fn validate_collisions(fields: &[FieldSpec]) -> syn::Result<()> {
470 let mut seen_long: std::collections::HashMap<String, Span> =
471 std::collections::HashMap::new();
472 let mut seen_short: std::collections::HashMap<char, Span> =
473 std::collections::HashMap::new();
474
475 let mut seen_optional = false;
477 for f in fields {
478 if !matches!(f.kind, FieldKind::Arg) {
479 continue;
480 }
481 let is_optional =
482 matches!(f.shape, TypeShape::Opt) || f.default.is_some() || f.variadic;
483 if seen_optional && !is_optional {
484 return Err(syn::Error::new(
485 f.span,
486 "required positional cannot follow an optional one",
487 ));
488 }
489 if is_optional {
490 seen_optional = true;
491 }
492 }
493 let mut saw_variadic = false;
495 for f in fields {
496 if !matches!(f.kind, FieldKind::Arg) {
497 continue;
498 }
499 if saw_variadic {
500 return Err(syn::Error::new(
501 f.span,
502 "variadic positional must be the last one",
503 ));
504 }
505 if f.variadic {
506 saw_variadic = true;
507 }
508 }
509
510 for f in fields {
511 if !matches!(f.kind, FieldKind::Option) {
512 continue;
513 }
514 let long = kebab(&f.ident.to_string());
515 if RESERVED_LONGS.contains(&long.as_str()) {
516 return Err(syn::Error::new(
517 f.span,
518 format!("--{long} shadows a reserved fdl-level flag"),
519 ));
520 }
521 if let Some(prev) = seen_long.insert(long.clone(), f.span) {
522 return Err(syn::Error::new(
523 f.span,
524 format!("duplicate long flag --{long} (previously declared at {:?})", prev),
525 ));
526 }
527 if let Some(s) = f.short {
528 if RESERVED_SHORTS.contains(&s) {
529 return Err(syn::Error::new(
530 f.span,
531 format!("-{s} shadows a reserved fdl-level flag"),
532 ));
533 }
534 if let Some(prev) = seen_short.insert(s, f.span) {
535 return Err(syn::Error::new(
536 f.span,
537 format!("duplicate short -{s} (previously declared at {:?})", prev),
538 ));
539 }
540 }
541 }
542
543 Ok(())
544}
545
546fn build_spec_expr(fields: &[FieldSpec]) -> TokenStream2 {
549 let opts = fields
550 .iter()
551 .filter(|f| matches!(f.kind, FieldKind::Option))
552 .map(build_option_decl);
553 let positionals = fields
554 .iter()
555 .filter(|f| matches!(f.kind, FieldKind::Arg))
556 .map(build_positional_decl);
557
558 quote! {
559 ::flodl_cli::args::parser::ArgsSpec {
560 options: vec![ #( #opts ),* ],
561 positionals: vec![ #( #positionals ),* ],
562 lenient_unknowns: false,
566 }
567 }
568}
569
570fn build_option_decl(f: &FieldSpec) -> TokenStream2 {
571 let long = kebab(&f.ident.to_string());
572 let takes_value = !matches!(f.shape, TypeShape::Bool);
573 let allows_bare = match f.shape {
574 TypeShape::Bool => true,
575 _ => f.default.is_some(),
576 };
577 let repeatable = matches!(f.shape, TypeShape::List);
578 let short_expr = match f.short {
579 Some(c) => quote! { ::std::option::Option::Some(#c) },
580 None => quote! { ::std::option::Option::None },
581 };
582 let choices_expr = match &f.choices {
583 Some(list) => {
584 let elems = list.iter();
585 quote! { ::std::option::Option::Some(vec![ #( ::std::string::String::from(#elems) ),* ]) }
586 }
587 None => quote! { ::std::option::Option::None },
588 };
589
590 quote! {
591 ::flodl_cli::args::parser::OptionDecl {
592 long: ::std::string::String::from(#long),
593 short: #short_expr,
594 takes_value: #takes_value,
595 allows_bare: #allows_bare,
596 repeatable: #repeatable,
597 choices: #choices_expr,
598 }
599 }
600}
601
602fn build_positional_decl(f: &FieldSpec) -> TokenStream2 {
603 let name = kebab(&f.ident.to_string());
604 let required = matches!(f.shape, TypeShape::Scalar) && f.default.is_none() && !f.variadic;
605 let variadic = f.variadic;
606 let choices_expr = match &f.choices {
607 Some(list) => {
608 let elems = list.iter();
609 quote! { ::std::option::Option::Some(vec![ #( ::std::string::String::from(#elems) ),* ]) }
610 }
611 None => quote! { ::std::option::Option::None },
612 };
613 quote! {
614 ::flodl_cli::args::parser::PositionalDecl {
615 name: ::std::string::String::from(#name),
616 required: #required,
617 variadic: #variadic,
618 choices: #choices_expr,
619 }
620 }
621}
622
623fn build_schema_expr(fields: &[FieldSpec], description: Option<&str>) -> TokenStream2 {
624 let desc_expr = match description {
625 Some(d) => quote! { ::std::option::Option::Some(::std::string::String::from(#d)) },
626 None => quote! { ::std::option::Option::None },
627 };
628
629 let option_inserts = fields
630 .iter()
631 .filter(|f| matches!(f.kind, FieldKind::Option))
632 .map(|f| {
633 let long = kebab(&f.ident.to_string());
634 let ty = schema_type_str(f);
635 let desc_expr = match &f.description {
636 Some(d) => quote! { ::std::option::Option::Some(::std::string::String::from(#d)) },
637 None => quote! { ::std::option::Option::None },
638 };
639 let default_expr = match &f.default {
640 Some(v) => quote! { ::std::option::Option::Some(::flodl_cli::serde_json::Value::String(::std::string::String::from(#v))) },
641 None => quote! { ::std::option::Option::None },
642 };
643 let choices_expr = match &f.choices {
644 Some(list) => {
645 let elems = list.iter();
646 quote! {
647 ::std::option::Option::Some(vec![
648 #( ::flodl_cli::serde_json::Value::String(::std::string::String::from(#elems)) ),*
649 ])
650 }
651 }
652 None => quote! { ::std::option::Option::None },
653 };
654 let short_expr = match f.short {
655 Some(c) => {
656 let cs = c.to_string();
657 quote! { ::std::option::Option::Some(::std::string::String::from(#cs)) }
658 }
659 None => quote! { ::std::option::Option::None },
660 };
661 let env_expr = match &f.env {
662 Some(v) => quote! { ::std::option::Option::Some(::std::string::String::from(#v)) },
663 None => quote! { ::std::option::Option::None },
664 };
665 let completer_expr = match &f.completer {
666 Some(v) => quote! { ::std::option::Option::Some(::std::string::String::from(#v)) },
667 None => quote! { ::std::option::Option::None },
668 };
669 quote! {
670 options.insert(
671 ::std::string::String::from(#long),
672 ::flodl_cli::OptionSpec {
673 ty: ::std::string::String::from(#ty),
674 description: #desc_expr,
675 default: #default_expr,
676 choices: #choices_expr,
677 short: #short_expr,
678 env: #env_expr,
679 completer: #completer_expr,
680 },
681 );
682 }
683 });
684
685 let arg_pushes = fields
686 .iter()
687 .filter(|f| matches!(f.kind, FieldKind::Arg))
688 .map(|f| {
689 let name = kebab(&f.ident.to_string());
690 let ty = schema_type_str(f);
691 let desc_expr = match &f.description {
692 Some(d) => quote! { ::std::option::Option::Some(::std::string::String::from(#d)) },
693 None => quote! { ::std::option::Option::None },
694 };
695 let required = matches!(f.shape, TypeShape::Scalar) && f.default.is_none() && !f.variadic;
696 let variadic = f.variadic;
697 let default_expr = match &f.default {
698 Some(v) => quote! { ::std::option::Option::Some(::flodl_cli::serde_json::Value::String(::std::string::String::from(#v))) },
699 None => quote! { ::std::option::Option::None },
700 };
701 let choices_expr = match &f.choices {
702 Some(list) => {
703 let elems = list.iter();
704 quote! {
705 ::std::option::Option::Some(vec![
706 #( ::flodl_cli::serde_json::Value::String(::std::string::String::from(#elems)) ),*
707 ])
708 }
709 }
710 None => quote! { ::std::option::Option::None },
711 };
712 let completer_expr = match &f.completer {
713 Some(v) => quote! { ::std::option::Option::Some(::std::string::String::from(#v)) },
714 None => quote! { ::std::option::Option::None },
715 };
716 quote! {
717 args.push(::flodl_cli::ArgSpec {
718 name: ::std::string::String::from(#name),
719 ty: ::std::string::String::from(#ty),
720 description: #desc_expr,
721 required: #required,
722 variadic: #variadic,
723 default: #default_expr,
724 choices: #choices_expr,
725 completer: #completer_expr,
726 });
727 }
728 });
729
730 let _ = desc_expr;
737 quote! {
738 {
739 let mut options: ::std::collections::BTreeMap<::std::string::String, ::flodl_cli::OptionSpec> =
740 ::std::collections::BTreeMap::new();
741 let mut args: ::std::vec::Vec<::flodl_cli::ArgSpec> = ::std::vec::Vec::new();
742 #( #option_inserts )*
743 #( #arg_pushes )*
744 ::flodl_cli::Schema {
745 args,
746 options,
747 strict: false,
748 }
749 }
750 }
751}
752
753fn schema_type_str(f: &FieldSpec) -> &'static str {
754 let inner = inner_ty_name(&f.inner_ty);
755 let base = match inner.as_str() {
756 "bool" => "bool",
757 "String" | "&str" => "string",
758 "PathBuf" | "Path" => "path",
759 "f32" | "f64" => "float",
760 "u8" | "u16" | "u32" | "u64" | "usize" | "i8" | "i16" | "i32" | "i64" | "isize" => "int",
762 _ => "string",
763 };
764 match f.shape {
765 TypeShape::List => match base {
766 "string" => "list[string]",
767 "int" => "list[int]",
768 "float" => "list[float]",
769 "path" => "list[path]",
770 _ => "list[string]",
771 },
772 TypeShape::Bool => "bool",
773 _ => base,
774 }
775}
776
777fn inner_ty_name(ty: &Type) -> String {
778 if let Type::Path(TypePath { path, .. }) = ty {
779 if let Some(seg) = path.segments.last() {
780 return seg.ident.to_string();
781 }
782 }
783 String::from("_")
784}
785
786fn build_env_injection(fields: &[FieldSpec]) -> TokenStream2 {
797 let mut injections: Vec<TokenStream2> = Vec::new();
798 for f in fields {
799 let Some(env_name) = f.env.as_deref() else {
800 continue;
801 };
802 if matches!(f.kind, FieldKind::Arg) {
805 continue;
806 }
807 let long = kebab(&f.ident.to_string());
808 let long_flag = format!("--{long}");
809 let long_eq_prefix = format!("--{long}=");
810 let short_tok = match &f.short {
811 Some(c) => {
812 let short_exact = format!("-{c}");
813 quote! {
814 || a.as_str() == #short_exact
815 }
816 }
817 None => quote! {},
818 };
819 injections.push(quote! {
820 {
821 let has_flag = __env_args.iter().any(|a: &::std::string::String| {
822 a.as_str() == #long_flag
823 || a.as_str().starts_with(#long_eq_prefix)
824 #short_tok
825 });
826 if !has_flag {
827 if let ::std::result::Result::Ok(v) = ::std::env::var(#env_name) {
828 if !v.is_empty() {
829 __env_args.push(::std::string::String::from(#long_flag));
830 __env_args.push(v);
831 }
832 }
833 }
834 }
835 });
836 }
837 if injections.is_empty() {
838 return quote! {};
839 }
840 quote! {
841 let __env_args: ::std::vec::Vec<::std::string::String> = {
842 let mut __env_args: ::std::vec::Vec<::std::string::String> = args.to_vec();
843 #( #injections )*
844 __env_args
845 };
846 let args: &[::std::string::String] = &__env_args[..];
847 }
848}
849
850fn build_extractor(ident: &Ident, fields: &[FieldSpec]) -> syn::Result<TokenStream2> {
851 let mut field_inits: Vec<TokenStream2> = Vec::new();
852 let mut positional_idx: usize = 0;
853 for f in fields {
854 match f.kind {
855 FieldKind::Option => field_inits.push(option_extraction(f)),
856 FieldKind::Arg => {
857 field_inits.push(arg_extraction(f, positional_idx));
858 if !f.variadic {
859 positional_idx += 1;
860 }
861 }
862 }
863 }
864 let field_names: Vec<&Ident> = fields.iter().map(|f| &f.ident).collect();
865 Ok(quote! {
866 #( #field_inits )*
867 ::std::result::Result::Ok(#ident {
868 #( #field_names ),*
869 })
870 })
871}
872
873fn option_extraction(f: &FieldSpec) -> TokenStream2 {
874 let ident = &f.ident;
875 let long = kebab(&ident.to_string());
876 let inner_ty = &f.inner_ty;
877 let span = ident.span();
878 let parse_one = quote_spanned! { span =>
879 |s: &::std::string::String| -> ::std::result::Result<#inner_ty, ::std::string::String> {
880 <#inner_ty as ::std::str::FromStr>::from_str(s)
881 .map_err(|e| format!("--{}: {}", #long, e))
882 }
883 };
884
885 match f.shape {
886 TypeShape::Bool => quote! {
887 let #ident: bool = matches!(
888 parsed.options.get(#long),
889 ::std::option::Option::Some(::flodl_cli::args::parser::OptionState::BarePresent)
890 );
891 },
892 TypeShape::Scalar => {
893 let default_lit = f.default.as_deref().unwrap();
895 quote! {
896 let #ident: #inner_ty = match parsed.options.get(#long) {
897 ::std::option::Option::Some(::flodl_cli::args::parser::OptionState::WithValues(v)) => {
898 let s = &v[0];
899 (#parse_one)(s)?
900 }
901 _ => {
902 let s = ::std::string::String::from(#default_lit);
903 (#parse_one)(&s).expect("default value must parse")
904 }
905 };
906 }
907 }
908 TypeShape::Opt => {
909 let default_tok = match &f.default {
910 Some(v) => quote! { ::std::option::Option::Some({
911 let s = ::std::string::String::from(#v);
912 (#parse_one)(&s).expect("default value must parse")
913 }) },
914 None => quote! { ::std::option::Option::None },
915 };
916 quote! {
917 let #ident: ::std::option::Option<#inner_ty> = match parsed.options.get(#long) {
918 ::std::option::Option::Some(::flodl_cli::args::parser::OptionState::WithValues(v)) => {
919 ::std::option::Option::Some((#parse_one)(&v[0])?)
920 }
921 ::std::option::Option::Some(::flodl_cli::args::parser::OptionState::BarePresent) => {
922 #default_tok
923 }
924 ::std::option::Option::None => ::std::option::Option::None,
925 };
926 }
927 }
928 TypeShape::List => quote! {
929 let #ident: ::std::vec::Vec<#inner_ty> = match parsed.options.get(#long) {
930 ::std::option::Option::Some(::flodl_cli::args::parser::OptionState::WithValues(v)) => {
931 let mut out: ::std::vec::Vec<#inner_ty> = ::std::vec::Vec::with_capacity(v.len());
932 for s in v {
933 out.push((#parse_one)(s)?);
934 }
935 out
936 }
937 _ => ::std::vec::Vec::new(),
938 };
939 },
940 }
941}
942
943fn arg_extraction(f: &FieldSpec, idx: usize) -> TokenStream2 {
944 let ident = &f.ident;
945 let name = kebab(&ident.to_string());
946 let inner_ty = &f.inner_ty;
947 let span = ident.span();
948 let parse_one = quote_spanned! { span =>
949 |s: &::std::string::String| -> ::std::result::Result<#inner_ty, ::std::string::String> {
950 <#inner_ty as ::std::str::FromStr>::from_str(s)
951 .map_err(|e| format!("<{}>: {}", #name, e))
952 }
953 };
954
955 match f.shape {
956 TypeShape::List if f.variadic => quote! {
957 let #ident: ::std::vec::Vec<#inner_ty> = {
958 let mut out: ::std::vec::Vec<#inner_ty> = ::std::vec::Vec::new();
959 for s in &parsed.positionals[#idx..] {
960 out.push((#parse_one)(s)?);
961 }
962 out
963 };
964 },
965 TypeShape::Opt => quote! {
966 let #ident: ::std::option::Option<#inner_ty> = match parsed.positionals.get(#idx) {
967 ::std::option::Option::Some(s) => ::std::option::Option::Some((#parse_one)(s)?),
968 ::std::option::Option::None => ::std::option::Option::None,
969 };
970 },
971 TypeShape::Scalar => {
972 let default_tok = match &f.default {
973 Some(v) => quote! {
974 {
975 let s = ::std::string::String::from(#v);
976 (#parse_one)(&s).expect("default value must parse")
977 }
978 },
979 None => quote! {
980 return ::std::result::Result::Err(
981 format!("missing required argument <{}>", #name)
982 )
983 },
984 };
985 quote! {
986 let #ident: #inner_ty = match parsed.positionals.get(#idx) {
987 ::std::option::Option::Some(s) => (#parse_one)(s)?,
988 ::std::option::Option::None => #default_tok,
989 };
990 }
991 }
992 _ => quote! {
993 compile_error!("unsupported positional type shape");
994 },
995 }
996}
997
998fn build_help_expr(fields: &[FieldSpec], description: Option<&str>, struct_name: &str) -> TokenStream2 {
999 let header = match description {
1003 Some(d) => format!("{d}\n\n"),
1004 None => format!("{struct_name}\n\n"),
1005 };
1006
1007 let mut arg_tokens: Vec<TokenStream2> = Vec::new();
1015 let mut opt_tokens: Vec<TokenStream2> = Vec::new();
1016
1017 for f in fields {
1018 match f.kind {
1019 FieldKind::Option => {
1020 let long = kebab(&f.ident.to_string());
1021 let short_prefix = match f.short {
1022 Some(c) => format!("-{c}, "),
1023 None => String::from(" "),
1024 };
1025 let value_part = match f.shape {
1026 TypeShape::Bool => String::new(),
1027 TypeShape::List => String::from(" <VALUE>..."),
1028 _ => format!(" <{}>", value_token(f)),
1029 };
1030 let label = format!("{short_prefix}--{long}{value_part}");
1031 let pad = " ".repeat(36usize.saturating_sub(4 + label.chars().count()));
1032 let mut tail = String::new();
1033 if let Some(d) = &f.description {
1034 tail.push_str(d);
1035 }
1036 if let Some(d) = &f.default {
1037 tail.push_str(&format!(" [default: {d}]"));
1038 }
1039 if let Some(choices) = &f.choices {
1040 tail.push_str(&format!(" [possible: {}]", choices.join(", ")));
1041 }
1042 opt_tokens.push(quote! {
1043 out.push_str(" ");
1044 out.push_str(&::flodl_cli::style::green(#label));
1045 out.push_str(#pad);
1046 out.push_str(#tail);
1047 out.push('\n');
1048 });
1049 }
1050 FieldKind::Arg => {
1051 let name = kebab(&f.ident.to_string());
1052 let required = matches!(f.shape, TypeShape::Scalar) && f.default.is_none();
1053 let label = if f.variadic {
1054 format!("<{name}>...")
1055 } else if required {
1056 format!("<{name}>")
1057 } else {
1058 format!("[<{name}>]")
1059 };
1060 let pad = " ".repeat(36usize.saturating_sub(4 + label.chars().count()));
1061 let mut tail = String::new();
1062 if let Some(d) = &f.description {
1063 tail.push_str(d);
1064 }
1065 if let Some(d) = &f.default {
1066 tail.push_str(&format!(" [default: {d}]"));
1067 }
1068 arg_tokens.push(quote! {
1069 out.push_str(" ");
1070 out.push_str(&::flodl_cli::style::green(#label));
1071 out.push_str(#pad);
1072 out.push_str(#tail);
1073 out.push('\n');
1074 });
1075 }
1076 }
1077 }
1078
1079 let arg_section = if arg_tokens.is_empty() {
1080 quote! {}
1081 } else {
1082 quote! {
1083 out.push_str(&::flodl_cli::style::yellow("Arguments"));
1084 out.push_str(":\n");
1085 #(#arg_tokens)*
1086 out.push('\n');
1087 }
1088 };
1089 let opt_section = if opt_tokens.is_empty() {
1090 quote! {}
1091 } else {
1092 quote! {
1093 out.push_str(&::flodl_cli::style::yellow("Options"));
1094 out.push_str(":\n");
1095 #(#opt_tokens)*
1096 out.push('\n');
1097 }
1098 };
1099
1100 quote! {
1101 {
1102 let mut out = ::std::string::String::from(#header);
1103 #arg_section
1104 #opt_section
1105 out
1106 }
1107 }
1108}
1109
1110fn value_token(f: &FieldSpec) -> &'static str {
1111 let inner = inner_ty_name(&f.inner_ty);
1112 match inner.as_str() {
1113 "u8" | "u16" | "u32" | "u64" | "usize" | "i8" | "i16" | "i32" | "i64" | "isize" => "N",
1114 "f32" | "f64" => "F",
1115 "PathBuf" | "Path" => "PATH",
1116 _ => "VALUE",
1117 }
1118}
1119
1120fn extract_doc(attrs: &[Attribute]) -> Option<String> {
1123 let mut lines: Vec<String> = Vec::new();
1124 for a in attrs {
1125 if !a.path().is_ident("doc") {
1126 continue;
1127 }
1128 if let syn::Meta::NameValue(nv) = &a.meta {
1129 if let Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) = &nv.value {
1130 let text = s.value();
1131 lines.push(text.trim().to_string());
1132 }
1133 }
1134 }
1135 if lines.is_empty() {
1136 return None;
1137 }
1138 let joined = lines.join(" ").split_whitespace().collect::<Vec<_>>().join(" ");
1140 if joined.is_empty() {
1141 None
1142 } else {
1143 Some(joined)
1144 }
1145}
1146
1147fn kebab(s: &str) -> String {
1148 s.replace('_', "-")
1149}
1150
1151use syn::spanned::Spanned;