tiny_cli/
lib.rs

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}