toad_macros/
lib.rs

1//! Macros used by `toad` for boilerplate reduction
2
3#![doc(html_root_url = "https://docs.rs/toad-macros/0.2.0")]
4#![cfg_attr(all(not(test), feature = "no_std"), no_std)]
5#![cfg_attr(not(test), forbid(missing_debug_implementations, unreachable_pub))]
6#![cfg_attr(not(test), deny(unsafe_code, missing_copy_implementations))]
7#![cfg_attr(any(docsrs, feature = "docs"), feature(doc_cfg))]
8#![deny(missing_docs)]
9
10use proc_macro::TokenStream;
11use quote::ToTokens;
12use regex::Regex;
13use syn::parse::Parse;
14use syn::{parse_macro_input, LitStr};
15
16struct DocSection(LitStr);
17
18impl Parse for DocSection {
19  fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
20    Ok(Self(input.parse::<LitStr>()?))
21  }
22}
23
24const RFC7252: &str = include_str!("./rfc7252.txt");
25
26/// Give me a section of RFC7252 (e.g. `5.9.1.1` no trailing dot)
27/// and I will scrape the rfc for that section then yield an inline `#[doc]` attribute containing that section.
28///
29/// ```
30/// use toad_macros::rfc_7252_doc;
31///
32/// #[doc = rfc_7252_doc!("5.9.1.1")]
33/// // Expands to:
34/// /// # 2.04 Changed
35/// /// [_generated from RFC7252 section 5.9.1.1_](<link to section at ietf.org>)
36/// ///
37/// /// This Response Code is like HTTP 204 "No Content" but only used in
38/// /// response to POST and PUT requests.  The payload returned with the
39/// /// response, if any, is a representation of the action result.
40/// ///
41/// /// This response is not cacheable.  However, a cache MUST mark any
42/// /// stored response for the changed resource as not fresh.
43/// struct Foo;
44/// ```
45#[proc_macro]
46pub fn rfc_7252_doc(input: TokenStream) -> TokenStream {
47  let DocSection(section_literal) = parse_macro_input!(input as DocSection);
48
49  let sec = section_literal.value();
50  let docstring = gen_docstring(sec, RFC7252);
51
52  LitStr::new(&docstring, section_literal.span()).to_token_stream()
53                                                 .into()
54}
55
56fn gen_docstring(sec: String, rfc: &'static str) -> String {
57  // Match {beginning of line}{section number} then capture everything until beginning of next section
58  let section_rx =
59    Regex::new(format!(r"(?s)\n{}\.\s+(.*?)(\n\d|$)", sec.replace('.', "\\.")).as_str()).unwrap_or_else(|e| {
60                                                                                      panic!("Section {} invalid: {:?}", sec, e)
61                                                                                    });
62  let rfc_section = section_rx.captures_iter(rfc)
63                              .next()
64                              .unwrap_or_else(|| panic!("Section {} not found", sec))
65                              .get(1)
66                              .unwrap_or_else(|| panic!("Section {} is empty", sec))
67                              .as_str();
68
69  let mut lines = trim_leading_ws(rfc_section);
70  let line1 = lines.drain(0..1)
71                   .next()
72                   .unwrap_or_else(|| panic!("Section {} is empty", sec));
73  let rest = lines.join("\n");
74
75  format!(
76          r"# {title}
77[_generated from RFC7252 section {section}_](https://datatracker.ietf.org/doc/html/rfc7252#section-{section})
78
79{body}",
80          title = line1,
81          section = sec,
82          body = rest
83  )
84}
85
86/// the RFC is formatted with 3-space indents in section bodies, with some addl
87/// indentation on some text.
88///
89/// This strips all leading whitespaces, except within code fences (&#96;&#96;&#96;), where it just trims the 3-space indent.
90///
91/// Returns the input string split by newlines
92fn trim_leading_ws(text: &str) -> Vec<String> {
93  #[derive(Clone, Copy)]
94  enum TrimStart {
95    Yes,
96    InCodeFence,
97  }
98
99  let trim_start = Regex::new(r"^ +").unwrap();
100  let trim_indent = Regex::new(r"^   ").unwrap();
101
102  text.split('\n')
103      .fold((Vec::<String>::new(), TrimStart::Yes),
104            |(mut lines, strip), s| {
105              let trimmed = trim_start.replace(s, "").to_string();
106              let dedented = trim_indent.replace(s, "").to_string();
107
108              let is_fence = trimmed.starts_with("```");
109
110              match (is_fence, strip) {
111                | (false, TrimStart::Yes) => {
112                  lines.push(trimmed);
113                  (lines, strip)
114                },
115                | (false, TrimStart::InCodeFence) => {
116                  lines.push(dedented);
117                  (lines, strip)
118                },
119                | (true, TrimStart::Yes) => {
120                  lines.push(trimmed);
121                  (lines, TrimStart::InCodeFence)
122                },
123                | (true, TrimStart::InCodeFence) => {
124                  lines.push(trimmed);
125                  (lines, TrimStart::Yes)
126                },
127              }
128            })
129      .0
130}
131
132#[cfg(test)]
133mod tests {
134  use super::*;
135
136  #[test]
137  fn rfcdoc_works() {
138    let rfc = r"
139Table of Contents
140
141   1.  Foo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
142     1.1.  Bingus .  . . . . . . . . . . . . . . . . . . . . . . . . . 2
143     1.2.  Terminology . . . . . . . . . . . . . . . . . . . . . . . . 3
144   2.  Bar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
145
1461. Foo
147   bar baz quux
148
149   ```text
150   dingus bar
151     foo
152   ```
1531.1.    Bingus
154   lorem ipsum frisky gypsum
155
1561.2. Terminology
157   Bat: tool used for baseball
158   Code: if (name === 'Jerry') {throw new Error('get out jerry!!1');}
159
1602. Bar
161   bingus
162   o fart
163   o poo";
164    // preserves whitespace, finds end of section that is not last
165    assert_eq!(
166               gen_docstring("1".into(), rfc),
167               r"# Foo
168[_generated from RFC7252 section 1_](https://datatracker.ietf.org/doc/html/rfc7252#section-1)
169
170bar baz quux
171
172```text
173dingus bar
174  foo
175```"
176    );
177
178    // finds end of section that is last
179    assert_eq!(
180               gen_docstring("2".into(), rfc),
181               r"# Bar
182[_generated from RFC7252 section 2_](https://datatracker.ietf.org/doc/html/rfc7252#section-2)
183
184bingus
185o fart
186o poo"
187    );
188  }
189}