use std::{error::Error, path::Path};
use microscpi_common::{Command, CommandPart};
use serde::Serialize;
use syn::{ImplItemFn, Lit, visit::Visit};
#[derive(Debug, Serialize)]
pub struct CommandDocumentation {
pub path: String,
pub name: String,
pub parts: Vec<CommandPart>,
pub is_query: bool,
pub description: Option<String>,
pub attributes: Option<serde_yaml::Value>,
}
fn parse_doc(doc_str: &str) -> (Option<String>, Option<serde_yaml::Value>) {
let clean_re = regex::Regex::new(r"(?m)^ ?").unwrap();
let doc_str = clean_re.replace_all(doc_str, "");
let re = regex::Regex::new(r"```yaml\s+([\s\S]+?)\s+```").unwrap();
let mut attributes = None;
let description = if let Some(captures) = re.captures(&doc_str) {
if let Some(yaml) = captures.get(1) {
match serde_yaml::from_str(yaml.as_str()) {
Ok(yaml) => {
attributes = Some(yaml);
re.replace(&doc_str, "").trim().to_string()
}
Err(_) => doc_str.to_string(),
}
} else {
doc_str.to_string()
}
} else {
doc_str.to_string()
};
(
if !description.is_empty() {
Some(description)
} else {
None
},
attributes,
)
}
#[derive(Debug, Serialize)]
pub struct Documentation {
pub commands: Vec<CommandDocumentation>,
}
impl Default for Documentation {
fn default() -> Self {
Self::new()
}
}
impl Documentation {
pub fn new() -> Self {
Self {
commands: Vec::new(),
}
}
pub fn parse_file(&mut self, path: impl AsRef<Path>) -> Result<(), Box<dyn Error>> {
let content = std::fs::read_to_string(path)?;
let file = syn::parse_file(content.as_str())?;
self.visit_file(&file);
Ok(())
}
pub fn add_command(&mut self, command: CommandDocumentation) {
self.commands.push(command);
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn write_to_file(&self, path: impl AsRef<Path>) -> std::io::Result<()> {
let content = self
.to_json()
.map_err(|e| std::io::Error::other(e.to_string()))?;
std::fs::write(path, content)
}
}
fn get_command_name(attr: &syn::Attribute) -> Option<String> {
let mut command_name = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("cmd") {
if let Lit::Str(name) = meta.value()?.parse()? {
command_name = Some(name.value());
Ok(())
} else {
Ok(())
}
} else {
Ok(())
}
});
command_name
}
fn get_command_doc(item_fn: &ImplItemFn) -> String {
let doc: String = item_fn
.attrs
.iter()
.filter(|attr| attr.path().is_ident("doc"))
.filter_map(|attr| {
if let syn::Meta::NameValue(meta) = attr.meta.clone() {
if let syn::Expr::Lit(expr_lit) = meta.value {
if let syn::Lit::Str(lit_str) = expr_lit.lit {
return Some(lit_str.value());
}
}
}
None
})
.collect::<Vec<String>>()
.join("\n");
doc
}
impl<'ast> Visit<'ast> for Documentation {
fn visit_impl_item_fn(&mut self, item_fn: &'ast ImplItemFn) {
for attr in &item_fn.attrs {
if attr.path().is_ident("scpi") {
let Some(cmd_name) = get_command_name(attr) else {
continue;
};
let doc = get_command_doc(item_fn);
let Ok(cmd) = Command::try_from(cmd_name.as_str()) else {
continue;
};
let (description, attributes) = parse_doc(doc.as_str());
let doc = CommandDocumentation {
path: cmd.canonical_path(),
name: cmd_name,
is_query: cmd.is_query(),
parts: cmd.parts,
description,
attributes,
};
self.add_command(doc);
}
}
}
}