use std::{
fmt::Display,
path::{Path, PathBuf},
str::Utf8Error,
};
use serde::Serialize;
use thiserror::Error;
pub use indexmap;
pub use serde_json;
pub use tree_sitter;
pub type Result<T> = std::result::Result<T, DossierError>;
#[derive(Error, Debug)]
pub enum DossierError {
UTF8Error(Utf8Error),
}
impl Display for DossierError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use DossierError::*;
match &self {
UTF8Error(error) => {
write!(f, "UTF8Error: {:?}", error)
}
}
}
}
pub type MarkdownString = String;
pub type FullyQualifiedName = String;
#[derive(Debug, Clone, Serialize, PartialEq)]
pub enum Identity {
#[serde(rename = "fqn")]
FQN(FullyQualifiedName),
#[serde(rename = "refers_to")]
Reference(FullyQualifiedName),
#[serde(skip_serializing)]
Anonymous,
}
impl Identity {
pub fn is_anonymous(&self) -> bool {
matches!(self, Identity::Anonymous)
}
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Entity {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub description: MarkdownString,
pub kind: String,
#[serde(flatten)]
#[serde(skip_serializing_if = "Identity::is_anonymous")]
pub identity: Identity,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub members: Vec<Entity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub member_context: Option<String>,
pub language: String,
pub source: Source,
#[serde(skip_serializing_if = "value_is_empty")]
pub meta: serde_json::Value,
}
fn value_is_empty(value: &serde_json::Value) -> bool {
value.is_null() || value.as_object().map(|o| o.is_empty()).unwrap_or(false)
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Position {
pub row: usize,
pub column: usize,
pub byte_offset: usize,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct Source {
pub file: PathBuf,
pub start: Position,
pub end: Position,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Context {
namespace: Vec<String>,
}
impl<'a> Context {
pub fn new() -> Self {
Self { namespace: vec![] }
}
pub fn generate_fqn<T>(&self, path: &Path, parts: T) -> String
where
T: IntoIterator<Item = &'a str>,
{
let mut fqn = format!("{}", path.display()).replace('\\', "/");
for part in &self.namespace {
fqn.push_str(&format!("::{}", part));
}
for part in parts {
fqn.push_str(&format!("::{}", part));
}
fqn
}
pub fn push_namespace(&mut self, namespace: &str) {
self.namespace.push(namespace.to_owned());
}
pub fn pop_namespace(&mut self) {
self.namespace.pop();
}
}
pub trait DocsParser {
fn parse<'a, P: Into<&'a Path>, T: IntoIterator<Item = P>>(
&self,
paths: T,
ctx: &mut Context,
) -> Result<Vec<Entity>>;
}
pub trait FileSource {
fn read_file<'a, P: Into<&'a Path>>(&self, path: P) -> std::io::Result<String>;
}
pub struct FileSystem;
impl FileSource for FileSystem {
fn read_file<'a, P: Into<&'a Path>>(&self, path: P) -> std::io::Result<String> {
std::fs::read_to_string(path.into())
}
}
pub struct InMemoryFileSystem {
pub files: indexmap::IndexMap<PathBuf, String>,
}
impl FileSource for InMemoryFileSystem {
fn read_file<'a, P: Into<&'a Path>>(&self, path: P) -> std::io::Result<String> {
let path: &Path = path.into();
self.files
.get(path)
.map(|s| s.to_owned())
.ok_or(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found: {}", path.display()),
))
}
}
pub mod helpers {
use super::*;
use tree_sitter::{Node, Query, QueryCapture};
pub fn node_for_capture<'a>(
name: &str,
captures: &'a [QueryCapture<'a>],
query: &Query,
) -> Option<Node<'a>> {
query
.capture_index_for_name(name)
.and_then(|index| captures.iter().find(|c| c.index == index))
.map(|c| c.node)
}
pub fn get_string_from_match<'a>(
captures: &'a [QueryCapture],
index: u32,
code: &'a str,
) -> Option<crate::Result<&'a str>> {
captures.iter().find(|c| c.index == index).map(|capture| {
capture
.node
.utf8_text(code.as_bytes())
.map_err(DossierError::UTF8Error)
})
}
}