simploxide_bindgen/
commands.rs

1//! Turns COMMANDS.md file into na Iterator of [`crate::commands::CommandResponse`].
2
3use convert_case::{Case, Casing as _};
4
5use crate::{
6    parse_utils,
7    types::{
8        DiscriminatedUnionType, RecordType, TopLevelDocs,
9        discriminated_union_type::DiscriminatedUnionVariant,
10    },
11};
12
13pub fn parse(commands_md: &str) -> impl Iterator<Item = Result<CommandResponse, String>> {
14    let mut parser = Parser::default();
15
16    commands_md
17        .split("---")
18        .skip(1)
19        .filter_map(|s| {
20            let trimmed = s.trim();
21            (!trimmed.is_empty()).then_some(trimmed)
22        })
23        .map(move |blk| parser.parse_block(blk))
24}
25
26pub struct CommandResponse {
27    pub command: RecordType,
28    pub response: DiscriminatedUnionType,
29}
30
31/// Generates the provided trait method for the ClientApi trait.
32///
33/// The ClientApi trait definition itself must be generated by the client and should look like this
34///
35/// ```ignore
36/// pub trait ClientApi {
37///     type Error;
38///
39///     fn send_raw(&self, command: String) -> impl Future<Output = Result<Arc<{}>, Self::Error>> + Send;
40///
41///     //..
42/// }
43/// ```
44///
45/// Then the provided methods could be inserted.
46///
47/// For methods that return multiple kinds a valid responses the additional wrapper type should be
48/// generated. You can get this type definition using the
49/// [`CommandResponseTraitMethod::response_wrapper`] method.
50pub struct CommandResponseTraitMethod<'a> {
51    pub command: &'a RecordType,
52    pub response: &'a DiscriminatedUnionType,
53}
54
55impl<'a> CommandResponseTraitMethod<'a> {
56    pub fn new(command: &'a RecordType, response: &'a DiscriminatedUnionType) -> Self {
57        Self { command, response }
58    }
59}
60
61impl<'a> CommandResponseTraitMethod<'a> {
62    /// If some method has multiple valid responses a helper type representing valid response
63    /// variants must be generated.
64    ///
65    /// If only one possible valid response is possible it can get inlined without extra helper
66    /// types and for this case this method returns `None`.
67    pub fn response_wrapper(&self) -> Option<ResponseWrapperFmt> {
68        if self.can_inline_response() {
69            return None;
70        }
71
72        Some(ResponseWrapperFmt(DiscriminatedUnionType::new(
73            self.response_wrapper_name(),
74            self.valid_responses().cloned().collect(),
75        )))
76    }
77
78    /// Instead of accepting a command type directly we can accept its arguments and construct it
79    /// internally.
80    ///
81    /// ```ignore
82    ///     fn api_show_my_address(cmd: ApiShowMyAddressCommand) -> ...
83    /// ```
84    ///
85    /// turns into
86    ///
87    /// ```ignore
88    ///     fn api_show_my_address(user_id: i64) -> ... {
89    ///         let cmd = ApiShowMyAddressCommand { user_id };
90    ///     }
91    /// ```
92    ///
93    /// This condition determines when such transformation takes place
94    fn can_inline_args(&self) -> bool {
95        !self
96            .command
97            .fields
98            .iter()
99            .any(|f| f.is_optional() || f.is_bool())
100    }
101
102    /// If a response consists only of a single valid variant this variant's inner struct can be
103    /// used directly as a return value of the API method.
104    fn can_inline_response(&self) -> bool {
105        self.valid_responses().count() == 1
106    }
107
108    fn valid_responses(&self) -> impl Iterator<Item = &'_ DiscriminatedUnionVariant> {
109        self.response
110            .variants
111            .iter()
112            .filter(|x| x.rust_name != "ChatCmdError")
113    }
114
115    fn response_wrapper_name(&self) -> String {
116        format!("{}s", self.response.name)
117    }
118}
119
120impl<'a> std::fmt::Display for CommandResponseTraitMethod<'a> {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        self.command.write_docs_fmt(f)?;
123        write!(f, "    fn {}(&self", self.command.name.to_case(Case::Snake))?;
124
125        let (ret_type, unwrapped_response_name) = if self.can_inline_response() {
126            let name = self.response.variants[0].fields[0].typ.clone();
127            (format!("Arc<{name}>"), name)
128        } else {
129            let name = self.response_wrapper_name();
130            (name.clone(), name)
131        };
132
133        if self.can_inline_args() {
134            for field in self.command.fields.iter() {
135                write!(f, ", {}: {}", field.rust_name, field.typ)?;
136            }
137
138            writeln!(
139                f,
140                ") -> impl Future<Output = Result<{ret_type}, Self::Error>> + Send {{ async move {{",
141            )?;
142            write!(f, "        let command = {} {{", self.command.name)?;
143
144            for (ix, field) in self.command.fields.iter().enumerate() {
145                if ix > 0 {
146                    write!(f, ", ")?;
147                }
148
149                write!(f, "{}", field.rust_name)?;
150            }
151            writeln!(f, "}};")?;
152        } else {
153            writeln!(
154                f,
155                ", command: {}) -> impl Future<Output = Result<{ret_type}, Self::Error>> + Send {{ async move {{",
156                self.command.name,
157            )?;
158        }
159
160        writeln!(
161            f,
162            "        let json = self.send_raw(command.interpret()).await?;"
163        )?;
164        writeln!(
165            f,
166            "        // Safe to unwrap because unrecognized JSON goes to undocumented variant"
167        )?;
168        writeln!(
169            f,
170            "        let response = serde_json::from_value(json).unwrap();"
171        )?;
172        writeln!(f, "        match response {{")?;
173
174        if self.can_inline_response() {
175            let first = self.valid_responses().next().unwrap();
176            writeln!(
177                f,
178                "            {}::{}(resp) => Ok(Arc::new(resp)),",
179                self.response.name, first.rust_name
180            )?;
181        } else {
182            for variant in self.valid_responses() {
183                writeln!(
184                    f,
185                    "            {}::{var_name}(resp) => Ok({}::{var_name}(Arc::new(resp))),",
186                    self.response.name,
187                    unwrapped_response_name,
188                    var_name = variant.rust_name,
189                )?;
190            }
191        }
192
193        writeln!(
194            f,
195            "            {}::ChatCmdError(resp) => Err(BadResponseError::ChatCmdError(Arc::new(resp)).into()),",
196            self.response.name,
197        )?;
198        writeln!(
199            f,
200            "            {}::Undocumented(resp) => Err(BadResponseError::Undocumented(resp).into()),",
201            self.response.name,
202        )?;
203        writeln!(f, "        }}")?;
204
205        writeln!(f, "    }}")?;
206        writeln!(f, "    }}")
207    }
208}
209
210/// Use this formatter for command types instead of the standard std::fmt::Display impl of the
211/// [`RecordType`]. This impl strips down all serialization attributes and undocumented fields.
212pub struct CommandFmt<'a>(pub &'a RecordType);
213
214impl std::fmt::Display for CommandFmt<'_> {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        self.0.write_docs_fmt(f)?;
217
218        writeln!(f, "#[derive(Debug, Clone, PartialEq)]")?;
219        writeln!(f, "#[cfg_attr(feature = \"bon\", derive(::bon::Builder))]")?;
220
221        writeln!(f, "pub struct {} {{", self.0.name)?;
222
223        for field in self.0.fields.iter() {
224            writeln!(f, "    pub {}: {},", field.rust_name, field.typ)?;
225        }
226
227        writeln!(f, "}}")
228    }
229}
230
231pub struct ResponseWrapperFmt(pub DiscriminatedUnionType);
232
233impl std::fmt::Display for ResponseWrapperFmt {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        writeln!(
236            f,
237            "#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
238        )?;
239        writeln!(f, "#[serde(tag = \"type\")]")?;
240        writeln!(f, "pub enum {} {{", self.0.name)?;
241
242        for variant in &self.0.variants {
243            for comment_line in &variant.doc_comments {
244                writeln!(f, "    /// {}", comment_line)?;
245            }
246            writeln!(f, "    #[serde(rename = \"{}\")]", variant.api_name)?;
247            writeln!(
248                f,
249                "    {}(Arc<{}>),",
250                variant.rust_name, variant.fields[0].typ
251            )?;
252        }
253        writeln!(f, "}}\n")?;
254
255        // Gen helper getters
256        writeln!(f, "impl {} {{", self.0.name)?;
257
258        for var in self.0.variants.iter() {
259            assert_eq!(var.fields.len(), 1, "Discriminated union is not disjointed");
260            assert!(
261                var.fields[0].rust_name.is_empty(),
262                "Discriminated union is not disjointed"
263            );
264
265            writeln!(
266                f,
267                "    pub fn {}(&self) -> Option<&{}> {{",
268                var.rust_name.to_case(Case::Snake),
269                var.fields[0].typ
270            )?;
271
272            writeln!(f, "        if let Self::{}(ret) = self {{", var.rust_name)?;
273            writeln!(f, "            Some(ret)",)?;
274            writeln!(f, "        }} else {{ None }}",)?;
275            writeln!(f, "    }}\n")?;
276        }
277
278        writeln!(f, "}}")
279    }
280}
281
282#[derive(Default)]
283struct Parser {
284    current_doc_section: Option<DocSection>,
285}
286
287impl Parser {
288    pub fn parse_block(&mut self, block: &str) -> Result<CommandResponse, String> {
289        self.parser(block.lines().map(str::trim))
290            .map_err(|e| format!("{e} in block\n```\n{block}\n```"))
291    }
292
293    fn parser<'a>(
294        &mut self,
295        mut lines: impl Iterator<Item = &'a str>,
296    ) -> Result<CommandResponse, String> {
297        const DOC_SECTION_PAT: &str = parse_utils::H2;
298        const TYPENAME_PAT: &str = parse_utils::H3;
299        const TYPEKINDS_PAT: &str = parse_utils::BOLD;
300
301        let mut next =
302            parse_utils::skip_empty(&mut lines).ok_or_else(|| "Got an empty block".to_owned())?;
303
304        let mut command_docs: Vec<String> = Vec::new();
305
306        let (typename, mut typekind) = loop {
307            if let Some(section_name) = next.strip_prefix(DOC_SECTION_PAT) {
308                let mut doc_section = DocSection::new(section_name.to_owned());
309
310                next = parse_utils::parse_doc_lines(&mut lines, &mut doc_section.contents, |s| {
311                    s.starts_with(TYPENAME_PAT)
312                })
313                .ok_or_else(|| format!("Failed to find a typename by pattern {TYPENAME_PAT:?} after the doc section"))?;
314
315                self.current_doc_section.replace(doc_section);
316            } else if let Some(name) = next.strip_prefix(TYPENAME_PAT) {
317                next = parse_utils::parse_doc_lines(&mut lines, &mut command_docs, |s| {
318                    s.starts_with(TYPEKINDS_PAT)
319                })
320                .map(|s| s.strip_prefix(TYPEKINDS_PAT).unwrap())
321                .ok_or_else(|| format!("Failed to find a typekind by pattern {TYPEKINDS_PAT:?} after the inner docs "))?;
322
323                break (name, next);
324            }
325        };
326
327        let command_name = typename.to_case(Case::Pascal);
328        let mut command = RecordType::new(command_name.clone(), vec![]);
329
330        loop {
331            if typekind.starts_with("Parameters") {
332                typekind = parse_utils::parse_record_fields(
333                    &mut lines,
334                    &mut command.fields,
335                    |s| s.starts_with(TYPEKINDS_PAT),
336                )?
337                .map(|s| s.strip_prefix(TYPEKINDS_PAT).unwrap())
338                .ok_or_else(|| format!(
339                    "Failed to find a command syntax after parameters by pattern {TYPENAME_PAT:?}"
340                ))?;
341            } else if typekind.starts_with("Syntax") {
342                parse_utils::parse_syntax(&mut lines, &mut command.syntax)?;
343                break;
344            }
345        }
346
347        let mut response_variants: Vec<DiscriminatedUnionVariant> = Vec::with_capacity(4);
348
349        parse_utils::skip_while(&mut lines, |s| !s.starts_with("**Response")).ok_or_else(|| {
350            "Failed to find responses section by pattern \"**Response\"".to_owned()
351        })?;
352
353        let mut variant_docline = Vec::new();
354
355        while let Some(docline) = parse_utils::skip_empty(&mut lines) {
356            if docline.starts_with(TYPEKINDS_PAT) {
357                break;
358            } else {
359                variant_docline.push(docline.to_owned());
360            }
361
362            let (mut variant, next) = parse_utils::parse_discriminated_union_variant(&mut lines)?;
363            assert!(next.map(|s| s.is_empty()).unwrap_or(true));
364            variant.doc_comments = std::mem::take(&mut variant_docline);
365            response_variants.push(variant);
366        }
367
368        let response =
369            DiscriminatedUnionType::new(format!("{command_name}Response"), response_variants);
370
371        if let Some(ref outer_docs) = self.current_doc_section {
372            command
373                .doc_comments
374                .push(format!("### {}", outer_docs.header.clone()));
375
376            command.doc_comments.push(String::new());
377
378            command
379                .doc_comments
380                .extend(outer_docs.contents.iter().cloned());
381
382            command.doc_comments.push(String::new());
383            command.doc_comments.push("----".to_owned());
384            command.doc_comments.push(String::new());
385        }
386
387        command.doc_comments.extend(command_docs);
388        Ok(CommandResponse { command, response })
389    }
390}
391
392#[derive(Default, Clone)]
393struct DocSection {
394    header: String,
395    contents: Vec<String>,
396}
397
398impl DocSection {
399    fn new(header: String) -> Self {
400        Self {
401            header,
402            contents: Vec::new(),
403        }
404    }
405}