1use std::{error::Error, path::Path};
2
3use microscpi_common::{Command, CommandPart};
4use serde::Serialize;
5use syn::{ImplItemFn, Lit, visit::Visit};
6
7#[derive(Debug, Serialize)]
9pub struct CommandDocumentation {
10 pub path: String,
12 pub name: String,
14 pub parts: Vec<CommandPart>,
16 pub is_query: bool,
18 pub description: Option<String>,
20 pub attributes: Option<serde_yaml::Value>,
22}
23
24fn parse_doc(doc_str: &str) -> (Option<String>, Option<serde_yaml::Value>) {
26 let clean_re = regex::Regex::new(r"(?m)^ ?").unwrap();
28 let doc_str = clean_re.replace_all(doc_str, "");
29
30 let re = regex::Regex::new(r"```yaml\s+([\s\S]+?)\s+```").unwrap();
32
33 let mut attributes = None;
34 let description = if let Some(captures) = re.captures(&doc_str) {
35 if let Some(yaml) = captures.get(1) {
36 match serde_yaml::from_str(yaml.as_str()) {
37 Ok(yaml) => {
38 attributes = Some(yaml);
39 re.replace(&doc_str, "").trim().to_string()
41 }
42 Err(_) => doc_str.to_string(),
43 }
44 } else {
45 doc_str.to_string()
46 }
47 } else {
48 doc_str.to_string()
49 };
50
51 (
52 if !description.is_empty() {
53 Some(description)
54 } else {
55 None
56 },
57 attributes,
58 )
59}
60
61#[derive(Debug, Serialize)]
63pub struct Documentation {
64 pub commands: Vec<CommandDocumentation>,
66}
67
68impl Default for Documentation {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl Documentation {
75 pub fn new() -> Self {
77 Self {
78 commands: Vec::new(),
79 }
80 }
81
82 pub fn parse_file(&mut self, path: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
84 let content = std::fs::read_to_string(path)?;
85 let file = syn::parse_file(content.as_str())?;
86 self.visit_file(&file);
87 Ok(())
88 }
89
90 pub fn add_command(&mut self, command: CommandDocumentation) {
92 self.commands.push(command);
93 }
94
95 pub fn to_json(&self) -> Result<String, serde_json::Error> {
97 serde_json::to_string_pretty(self)
98 }
99
100 pub fn write_to_file(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
102 let content = self
103 .to_json()
104 .map_err(|e| std::io::Error::other(e.to_string()))?;
105
106 std::fs::write(path, content)
107 }
108}
109
110fn get_command_name(attr: &syn::Attribute) -> Option<String> {
112 let mut command_name = None;
113
114 let _ = attr.parse_nested_meta(|meta| {
115 if meta.path.is_ident("cmd") {
116 if let Lit::Str(name) = meta.value()?.parse()? {
117 command_name = Some(name.value());
118 Ok(())
119 } else {
120 Ok(())
121 }
122 } else {
123 Ok(())
124 }
125 });
126
127 command_name
128}
129
130fn get_command_doc(item_fn: &ImplItemFn) -> String {
132 let doc: String = item_fn
133 .attrs
134 .iter()
135 .filter(|attr| attr.path().is_ident("doc"))
136 .filter_map(|attr| {
137 if let syn::Meta::NameValue(meta) = attr.meta.clone() {
138 if let syn::Expr::Lit(expr_lit) = meta.value {
139 if let syn::Lit::Str(lit_str) = expr_lit.lit {
140 return Some(lit_str.value());
141 }
142 }
143 }
144 None
145 })
146 .collect::<Vec<String>>()
147 .join("\n");
148 doc
149}
150
151impl<'ast> Visit<'ast> for Documentation {
152 fn visit_impl_item_fn(&mut self, item_fn: &'ast ImplItemFn) {
153 for attr in &item_fn.attrs {
154 if attr.path().is_ident("scpi") {
155 let Some(cmd_name) = get_command_name(attr) else {
156 continue;
157 };
158
159 let doc = get_command_doc(item_fn);
160
161 let Ok(cmd) = Command::try_from(cmd_name.as_str()) else {
162 continue;
163 };
164
165 let (description, attributes) = parse_doc(doc.as_str());
166
167 let doc = CommandDocumentation {
168 path: cmd.canonical_path(),
169 name: cmd_name,
170 is_query: cmd.is_query(),
171 parts: cmd.parts,
172 description,
173 attributes,
174 };
175
176 self.add_command(doc);
177 }
178 }
179 }
180}