use std::{collections::HashMap, error::Error, path::Path};
use gray_matter::{engine::YAML, Matter};
use serde::{Deserialize, Serialize};
#[cfg(feature = "python")]
use pyo3::pyclass;
#[cfg(feature = "wasm")]
use tsify_next::Tsify;
#[cfg(not(target_arch = "wasm32"))]
use crate::git;
use crate::prelude::DataModel;
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
pub struct FrontMatter {
pub id: Option<String>,
#[serde(default = "default_id_field", rename = "id-field")]
pub id_field: bool,
pub prefixes: Option<HashMap<String, String>>,
pub nsmap: Option<HashMap<String, String>>,
#[serde(default = "default_repo", alias = "iri")]
pub repo: String,
#[serde(default = "default_prefix")]
pub prefix: String,
#[serde(default)]
pub imports: HashMap<String, ImportType>,
#[serde(default = "default_allow_empty", rename = "allow-empty")]
pub allow_empty: bool,
}
impl FrontMatter {
pub fn new() -> Self {
FrontMatter {
id: None,
id_field: default_id_field(),
prefixes: None,
nsmap: None,
repo: default_repo(),
prefix: default_prefix(),
imports: HashMap::new(),
allow_empty: false,
}
}
pub fn id_field(&self) -> bool {
self.id_field
}
pub fn prefixes(&self) -> Option<Vec<(String, String)>> {
self.prefixes.as_ref().map(|prefixes| {
prefixes
.iter()
.map(|(k, v)| {
let with_slash = if v.ends_with('/') {
v.clone()
} else {
format!("{}/", v)
};
(k.clone(), with_slash)
})
.collect()
})
}
pub fn nsmap(&self) -> &Option<HashMap<String, String>> {
&self.nsmap
}
}
#[derive(Debug, Serialize, Clone, PartialEq)]
#[cfg_attr(feature = "python", pyclass(get_all, from_py_object))]
#[cfg_attr(feature = "wasm", derive(Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
pub enum ImportType {
Remote(String),
Local(String),
}
impl<'de> Deserialize<'de> for ImportType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s.starts_with("http://") || s.starts_with("https://") {
Ok(ImportType::Remote(s))
} else {
Ok(ImportType::Local(s))
}
}
}
impl ImportType {
pub fn fetch(&self, dirpath: Option<&Path>) -> Result<DataModel, Box<dyn Error>> {
match self {
ImportType::Remote(url) => self.fetch_remote_model(url),
ImportType::Local(path) => self.fetch_local_model(path, dirpath),
}
}
fn fetch_remote_model(&self, url: &str) -> Result<DataModel, Box<dyn Error>> {
#[cfg(not(target_arch = "wasm32"))]
if let Some((repo, path)) = git::parse_github_file_url(url) {
return DataModel::from_github(&repo, &path);
}
Err(format!("Unsupported remote import URL: {url}").into())
}
fn fetch_local_model(
&self,
path: &str,
dirpath: Option<&Path>,
) -> Result<DataModel, Box<dyn Error>> {
let path = if let Some(dirpath) = dirpath {
dirpath.parent().unwrap().join(path).display().to_string()
} else {
path.to_string()
};
let path = std::fs::canonicalize(path)?;
let model = DataModel::from_markdown(&path)?;
Ok(model)
}
}
impl Default for FrontMatter {
fn default() -> Self {
Self::new()
}
}
fn default_id_field() -> bool {
true
}
fn default_prefix() -> String {
"md".to_string()
}
fn default_repo() -> String {
"http://mdmodel.net".to_string()
}
fn default_allow_empty() -> bool {
false
}
pub fn parse_frontmatter(content: &str) -> Option<FrontMatter> {
let matter = Matter::<YAML>::new();
matter.parse(content).ok()?.data
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use std::path::Path;
use super::*;
#[test]
fn test_parse_frontmatter() {
let path = Path::new("tests/data/model.md");
let content = std::fs::read_to_string(path).expect("Could not read file");
let frontmatter = parse_frontmatter(&content)
.expect("Could not parse frontmatter from file. Please check the file content.");
assert_eq!(frontmatter.id_field, true);
assert_eq!(
frontmatter.prefixes.unwrap().get("schema").unwrap(),
"http://schema.org/"
);
assert_eq!(
frontmatter.nsmap.unwrap().get("tst").unwrap(),
"http://example.com/test/"
);
}
}