simploxide_bindgen/types/
mod.rs1pub mod discriminated_union_type;
9pub mod enum_type;
10pub mod record_type;
11
12pub use discriminated_union_type::{
13 DiscriminatedUnionType, DisjointedDiscriminatedUnion, DisjointedDisriminatedUnionVariant,
14};
15pub use enum_type::EnumType;
16pub use record_type::RecordType;
17
18use convert_case::{Case, Casing as _};
19use std::str::FromStr;
20
21use crate::parse_utils;
22
23pub fn parse(types_md: &str) -> impl Iterator<Item = Result<ApiType, String>> {
24 types_md.split("---").skip(1).map(ApiType::from_str)
25}
26
27pub enum ApiType {
28 Record(RecordType),
29 DiscriminatedUnion(DiscriminatedUnionType),
30 Enum(EnumType),
31}
32
33impl ApiType {
34 pub fn is_error(&self) -> bool {
36 self.name().contains("Error")
37 }
38
39 pub fn name(&self) -> &str {
40 match self {
41 Self::Record(r) => r.name.as_str(),
42 Self::Enum(e) => e.name.as_str(),
43 Self::DiscriminatedUnion(du) => du.name.as_str(),
44 }
45 }
46}
47
48impl std::str::FromStr for ApiType {
49 type Err = String;
50
51 fn from_str(md_block: &str) -> Result<Self, Self::Err> {
52 fn parser<'a>(mut lines: impl Iterator<Item = &'a str>) -> Result<ApiType, String> {
53 const TYPENAME_PAT: &str = parse_utils::H2;
54 const TYPEKIND_PAT: &str = parse_utils::BOLD;
55
56 let typename = parse_utils::skip_empty(&mut lines)
57 .and_then(|s| s.strip_prefix(TYPENAME_PAT))
58 .ok_or_else(|| format!("Failed to find a type name by pattern {TYPENAME_PAT:?}"))?;
59
60 let mut doc_comments = Vec::new();
61
62 let typekind = parse_utils::parse_doc_lines(&mut lines, &mut doc_comments, |s| {
63 s.starts_with(TYPEKIND_PAT)
64 })
65 .map(|s| s.strip_prefix(TYPEKIND_PAT).unwrap())
66 .ok_or_else(|| format!("Failed to find a type kind by pattern {TYPEKIND_PAT:?}"))?;
67
68 let mut syntax = String::new();
69 let breaker = |s: &str| s.starts_with("**Syntax");
70
71 if typekind.starts_with("Record") {
72 let mut fields = Vec::new();
73
74 let syntax_block =
75 parse_utils::parse_record_fields(&mut lines, &mut fields, breaker)?;
76
77 if syntax_block.is_some() {
78 parse_utils::parse_syntax(&mut lines, &mut syntax)?;
79 }
80
81 Ok(ApiType::Record(RecordType {
82 name: typename.to_owned(),
83 fields,
84 doc_comments,
85 syntax,
86 }))
87 } else if typekind.starts_with("Enum") {
88 let mut variants = Vec::new();
89
90 let syntax_block =
91 parse_utils::parse_enum_variants(&mut lines, &mut variants, breaker)?;
92
93 if syntax_block.is_some() {
94 parse_utils::parse_syntax(&mut lines, &mut syntax)?;
95 }
96
97 Ok(ApiType::Enum(EnumType {
98 name: typename.to_owned(),
99 variants,
100 doc_comments,
101 syntax,
102 }))
103 } else if typekind.starts_with("Discriminated") {
104 let mut variants = Vec::new();
105
106 let syntax_block = parse_utils::parse_discriminated_union_variants(
107 &mut lines,
108 &mut variants,
109 breaker,
110 )?;
111
112 if syntax_block.is_some() {
113 parse_utils::parse_syntax(&mut lines, &mut syntax)?;
114 }
115
116 Ok(ApiType::DiscriminatedUnion(DiscriminatedUnionType {
117 name: typename.to_owned(),
118 variants,
119 doc_comments,
120 syntax,
121 }))
122 } else {
123 Err(format!("Unknown type kind: {typekind:?}"))
124 }
125 }
126
127 parser(md_block.lines().map(str::trim))
128 .map_err(|e| format!("{e} in md block\n```\n{md_block}\n```"))
129 }
130}
131
132impl std::fmt::Display for ApiType {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self {
135 Self::Record(r) => r.fmt(f),
136 Self::Enum(e) => e.fmt(f),
137 Self::DiscriminatedUnion(du) => du.fmt(f),
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq)]
144pub enum Source {
145 Types,
146 Commands,
147 Events,
148}
149
150#[derive(Debug, Clone, PartialEq)]
151pub struct Field {
152 pub api_name: String,
153 pub rust_name: String,
154 pub typ: String,
155 pub source: Option<Source>,
158}
159
160impl Field {
161 pub fn from_api_name(api_name: String, typ: String) -> Self {
162 Self {
163 api_name: api_name.clone(),
164 rust_name: api_name.remove_empty().to_case(Case::Snake),
165 typ,
166 source: None,
167 }
168 }
169 pub fn is_optional(&self) -> bool {
170 is_optional_type(self.typ.as_str())
171 }
172
173 pub fn is_vec(&self) -> bool {
174 is_vec_type(self.typ.as_str())
175 }
176
177 pub fn is_map(&self) -> bool {
178 is_map_type(self.typ.as_str())
179 }
180
181 pub fn is_numeric(&self) -> bool {
182 is_numeric_type(self.typ.as_str())
183 }
184
185 pub fn is_bool(&self) -> bool {
186 is_bool_type(self.typ.as_str())
187 }
188
189 pub fn is_string(&self) -> bool {
190 is_string_type(self.typ.as_str())
191 }
192
193 pub fn is_compound(&self) -> bool {
194 is_compound_type(self.typ.as_str())
195 }
196
197 pub fn is_error(&self) -> bool {
199 self.typ.contains("Error")
200 }
201
202 pub fn inner_type(&self) -> Option<&str> {
206 inner_type(self.typ.as_str())
207 }
208
209 pub fn inner_type_offset(&self) -> Option<usize> {
211 inner_type_offset(self.typ.as_str())
212 }
213
214 pub fn base_type(&self) -> &str {
220 let mut ret = self.typ.as_str();
221
222 while let Some(inner) = inner_type(ret) {
223 ret = inner
224 }
225
226 ret
227 }
228
229 pub fn base_type_offset(&self) -> usize {
231 let mut ret = 0;
232
233 while let Some(offset) = inner_type_offset(&self.typ[ret..]) {
234 ret += offset;
235 }
236
237 ret
238 }
239}
240
241impl FromStr for Field {
242 type Err = String;
243
244 fn from_str(line: &str) -> Result<Self, Self::Err> {
245 let (name, typ) = line
246 .trim()
247 .split_once(':')
248 .ok_or_else(|| format!("Failed to parse field at line: '{line}'"))?;
249
250 let api_name = name.trim().to_owned();
251 let rust_name = api_name.remove_empty().to_case(Case::Snake);
252 let raw_typ = typ.trim();
253 let typ = resolve_type(raw_typ)?;
254 let source = parse_field_source(raw_typ);
255
256 Ok(Field {
257 api_name,
258 rust_name,
259 typ,
260 source,
261 })
262 }
263}
264
265pub fn is_optional_type(typ: &str) -> bool {
266 typ.starts_with("Option<")
267}
268
269pub fn is_vec_type(typ: &str) -> bool {
270 typ.starts_with("Vec<")
271}
272
273pub fn is_map_type(typ: &str) -> bool {
274 typ.starts_with("BTreeMap<")
275}
276
277pub fn is_numeric_type(typ: &str) -> bool {
278 typ.starts_with("i")
279 || typ.starts_with("f")
280 || typ.starts_with("u")
281 || typ.starts_with("Option<i")
282 || typ.starts_with("Option<f")
283 || typ.starts_with("Option<u")
284}
285
286pub fn is_bool_type(typ: &str) -> bool {
287 typ == "bool"
288}
289
290pub fn is_string_type(typ: &str) -> bool {
291 typ == "String" || typ == "UtcTime"
292}
293
294pub fn is_compound_type(typ: &str) -> bool {
295 !is_optional_type(typ)
296 && !is_vec_type(typ)
297 && !is_map_type(typ)
298 && !is_numeric_type(typ)
299 && !is_bool_type(typ)
300 && !is_string_type(typ)
301}
302
303pub fn inner_type(typ: &str) -> Option<&str> {
307 let start = inner_type_offset(typ)?;
308 let end = typ.rfind('>')?;
309 Some(&typ[start..end])
310}
311
312pub fn inner_type_offset(typ: &str) -> Option<usize> {
313 if typ.strip_prefix("Option<").is_some() {
314 Some("Option<".len())
315 } else if typ.strip_prefix("Vec<").is_some() {
316 Some("Vec<".len())
317 } else if let Some(s) = typ.strip_prefix("BTreeMap<") {
318 let mut total_offset = s.find(',').unwrap();
319 total_offset += s[total_offset..].find(char::is_alphabetic).unwrap();
320 Some("BTreeMap<".len() + total_offset)
321 } else {
322 None
323 }
324}
325
326fn parse_field_source(raw_typ: &str) -> Option<Source> {
331 const LINK_START: &str = "](";
332 let url_start = raw_typ.find(LINK_START)?;
333 let rest = &raw_typ[url_start + LINK_START.len()..];
334 let url_end = rest.find(')')?;
335 let url = &rest[..url_end];
336 if url.contains("TYPES.md") {
337 Some(Source::Types)
338 } else if url.contains("COMMANDS.md") {
339 Some(Source::Commands)
340 } else if url.contains("EVENTS.md") {
341 Some(Source::Events)
342 } else {
343 None
344 }
345}
346
347fn resolve_type(t: &str) -> Result<String, String> {
348 if let Some(t) = t.strip_suffix('}') {
349 let t = t.strip_prefix('{').unwrap().trim();
350 let (lhs, rhs) = t.split_once(':').unwrap();
351
352 let key = resolve_type(lhs.trim())?;
353 let val = resolve_type(rhs.trim())?;
354
355 return Ok(format!("BTreeMap<{key}, {val}>"));
356 }
357
358 if let Some(t) = t.strip_suffix(']') {
359 let resolved = resolve_type(t.strip_prefix('[').unwrap())?;
360 return Ok(format!("Vec<{resolved}>"));
361 }
362
363 if let Some(t) = t.strip_suffix('?') {
364 let resolved = resolve_type(t)?;
365 return Ok(format!("Option<{resolved}>"));
366 }
367
368 let resolved = match t {
369 "bool" => "bool".to_owned(),
370 "int" => "i32".to_owned(),
371 "int64" => "i64".to_owned(),
372 "word32" => "u32".to_owned(),
373 "double" => "f64".to_owned(),
374 "string" => "String".to_owned(),
375 "UTCTime" => "UtcTime".to_owned(),
380 "JSONObject" => "JsonObject".to_owned(),
381
382 compound if compound.starts_with('[') => {
383 let end = compound.find(']').unwrap();
384 compound['['.len_utf8()..end].to_owned()
385 }
386
387 _ => return Err(format!("Failed to resolve type: `{t}`")),
388 };
389
390 Ok(resolved)
391}
392
393pub struct FieldFmt<'a> {
395 field: &'a Field,
396 offset: usize,
397 is_pub: bool,
398}
399
400impl<'a> FieldFmt<'a> {
401 pub fn new(field: &'a Field) -> Self {
402 Self::with_offset(field, 0)
403 }
404
405 pub fn with_offset(field: &'a Field, offset: usize) -> Self {
406 Self {
407 field,
408 offset,
409 is_pub: false,
410 }
411 }
412
413 pub fn set_pub(&mut self, new: bool) {
414 self.is_pub = new;
415 }
416}
417
418impl<'a> std::fmt::Display for FieldFmt<'a> {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 let offset = " ".repeat(self.offset);
421 let pub_ = if self.is_pub { "pub " } else { "" };
422 let is_numeric = self.field.is_numeric();
423 let is_bool = self.field.is_bool();
424 let is_optional = self.field.is_optional();
425
426 write!(f, "{offset}#[serde(rename = \"{}\"", self.field.api_name)?;
427
428 if is_optional {
429 write!(f, ", skip_serializing_if = \"Option::is_none\"")?;
430 }
431
432 if is_numeric {
433 if is_optional {
434 write!(
435 f,
436 ", deserialize_with=\"deserialize_option_number_from_string\", default"
437 )?;
438 } else {
439 write!(f, ", deserialize_with=\"deserialize_number_from_string\"")?;
440 }
441 } else if is_bool {
442 write!(f, ", default")?;
443 }
444
445 writeln!(f, ")]")?;
446 writeln!(
447 f,
448 "{offset}{pub_}{}: {},",
449 self.field.rust_name, self.field.typ
450 )?;
451
452 Ok(())
453 }
454}
455
456pub(crate) fn convert_doc_links(line: &str) -> std::borrow::Cow<'_, str> {
459 if !line.contains("](") {
460 return std::borrow::Cow::Borrowed(line);
461 }
462
463 let mut result = String::with_capacity(line.len());
464 let mut remaining = line;
465
466 while let Some(bracket_pos) = remaining.find('[') {
467 let after_open = &remaining[bracket_pos + '['.len_utf8()..];
468 if let Some(close_bracket) = after_open.find("](") {
469 let display = &after_open[..close_bracket];
470 let after_close = &after_open[close_bracket + "](".len()..];
471 if let Some(paren_close) = after_close.find(')') {
472 let url = &after_close[..paren_close];
473 if url.starts_with('#') || url.contains(".md") {
474 result.push_str(&remaining[..bracket_pos]);
475 result.push('[');
476 result.push_str(&display.remove_empty().to_case(Case::Pascal));
477 result.push(']');
478 remaining = &after_close[paren_close + ')'.len_utf8()..];
479 continue;
480 }
481 }
482 }
483 result.push_str(&remaining[..bracket_pos + '['.len_utf8()]);
485 remaining = &remaining[bracket_pos + '['.len_utf8()..];
486 }
487
488 result.push_str(remaining);
489 std::borrow::Cow::Owned(result)
490}
491
492pub(crate) trait TopLevelDocs {
494 fn doc_lines(&self) -> &Vec<String>;
495
496 fn syntax(&self) -> &str;
497
498 fn write_docs_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
499 for line in self.doc_lines() {
500 writeln!(f, "/// {}", convert_doc_links(line))?;
501 }
502
503 if !self.syntax().is_empty() {
504 if !self.doc_lines().is_empty() {
505 writeln!(f, "///")?;
506 }
507
508 writeln!(f, "/// *Syntax:*")?;
509 writeln!(f, "///")?;
510 writeln!(f, "/// ```")?;
511 writeln!(f, "/// {}", self.syntax())?;
512 writeln!(f, "/// ```")?;
513 }
514
515 Ok(())
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[test]
524 fn inner_type_test() {
525 let option = "Option<String>";
526 let vec = "Vec<i64>";
527 let map = "BTreeMap<i64, Option<Vec<JsonObject>>>";
528 let regular = "String";
529
530 assert_eq!(inner_type(option), Some("String"));
531 assert_eq!(inner_type(vec), Some("i64"));
532 let map_value = inner_type(map).unwrap();
533 assert_eq!(map_value, "Option<Vec<JsonObject>>");
534 let inner_vec = inner_type(map_value).unwrap();
535 assert_eq!(inner_vec, "Vec<JsonObject>");
536 let json = inner_type(inner_vec).unwrap();
537 assert_eq!(json, "JsonObject");
538
539 assert_eq!(inner_type(json), None);
540 assert_eq!(inner_type(regular), None);
541
542 let mut option = String::from(option);
543 let offset = inner_type_offset(&option).unwrap();
544 option.insert_str(offset, "NotA");
545
546 assert_eq!(option, "Option<NotAString>");
547 }
548}