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