1use 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
31pub 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 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 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 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
210pub 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 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}