1#![warn(clippy::pedantic)]
2mod derive_struct;
3mod subcommand;
4
5extern crate proc_macro;
6
7use proc_macro::{Group, Ident, Literal, TokenStream, TokenTree};
8use std::fmt::Display;
9
10#[proc_macro_derive(ArgParse, attributes(cli))]
11pub fn derive_arg_parse(struct_candidate: TokenStream) -> TokenStream {
12 derive_struct::do_derive(struct_candidate)
13}
14
15#[proc_macro_derive(Subcommand, attributes(cli))]
16pub fn derive_sc_parse(struct_candidate: TokenStream) -> TokenStream {
17 subcommand::do_derive(struct_candidate)
18}
19
20fn pop_expect_punct<I: Iterator<Item = TokenTree>, D: Display>(
21 stream: &mut I,
22 expect: char,
23 err_msg: D,
24) {
25 let punct = stream
26 .next()
27 .unwrap_or_else(|| panic!("[ArgParse derive] {err_msg}"));
28 if let TokenTree::Punct(p) = punct {
29 assert_eq!(p.as_char(), expect, "{err_msg}");
30 } else {
31 panic!(
32 "[ArgParse derive] Expected punctuation with {expect}, found: {punct:?}, ctx: {err_msg}"
33 );
34 }
35}
36
37fn pop_expect_ident<I: Iterator<Item = TokenTree>, D: Display>(
38 stream: &mut I,
39 expect: &str,
40 err_msg: D,
41) {
42 let ident = pop_ident(stream, &err_msg);
43 assert_eq!(
44 expect,
45 ident.to_string().trim(),
46 "[ArgParse derive] Ident {ident} didn't match expected {expect}, ctx: {err_msg}"
47 );
48}
49
50fn pop_ident<I: Iterator<Item = TokenTree>, D: Display>(stream: &mut I, err_msg: D) -> Ident {
51 let ident = stream
52 .next()
53 .unwrap_or_else(|| panic!("[ArgParse derive] {err_msg}"));
54 if let TokenTree::Ident(ident) = ident {
55 ident
56 } else {
57 panic!("[ArgParse derive] Expected ident, found '{ident:?}', ctx: {err_msg}");
58 }
59}
60
61fn pop_lit<I: Iterator<Item = TokenTree>, D: Display>(stream: &mut I, err_msg: D) -> Literal {
62 let lit = stream
63 .next()
64 .unwrap_or_else(|| panic!("[ArgParse derive] {err_msg}"));
65 if let TokenTree::Literal(l) = lit {
66 l
67 } else {
68 panic!("[ArgParse derive] Expected literal found: {lit:?}, ctx: {err_msg}");
69 }
70}
71
72fn pop_group<I: Iterator<Item = TokenTree>, D: Display>(stream: &mut I, err_msg: D) -> Group {
73 let group = stream
74 .next()
75 .unwrap_or_else(|| panic!("[ArgParse derive] {err_msg}"));
76 if let TokenTree::Group(g) = group {
77 g
78 } else {
79 panic!("[ArgParse derive] Expected group, found: {group:?}, ctx: {err_msg}");
80 }
81}
82
83pub(crate) fn try_extract_doc_comment(g: &Group) -> Option<String> {
84 let mut stream = g.stream().into_iter();
85 if let Some(TokenTree::Ident(id)) = stream.next() {
86 if id.to_string().trim() == "doc" {
87 pop_expect_punct(&mut stream, '=', "Expected a '=' after #[doc");
88 let ident = pop_lit(&mut stream, "Expected #[doc = <literal>...");
89 let id_str = ident.to_string();
90 let id_trimmed = id_str.trim().trim_matches('"').trim().to_string();
91 return Some(id_trimmed);
92 }
93 }
94 None
95}
96
97pub(crate) fn pascal_to_snake(prev: &str) -> String {
98 let mut new = String::new();
99 let mut chars = prev.chars();
100 if let Some(next) = chars.next() {
101 for lc in next.to_lowercase() {
102 new.push(lc);
103 }
104 } else {
105 return new;
106 }
107 for char in chars {
108 if char.is_uppercase() {
109 new.push('-');
110 }
111 for lc in char.to_lowercase() {
112 new.push(lc);
113 }
114 }
115 new
116}
117
118#[inline]
119pub(crate) fn snake_to_scream(prev: &str) -> String {
120 prev.replace('-', "_").to_uppercase()
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn switch_case() {
129 let orig = "MyStructDecl";
130 assert_eq!("my-struct-decl", pascal_to_snake(orig));
131 let orig = "M";
132 assert_eq!("m", pascal_to_snake(orig));
133 assert_eq!("", pascal_to_snake(""));
134 assert_eq!("m-m", pascal_to_snake("MM"));
135 }
136}