use crate::parse::parse_str;
use codespan_reporting::files::SimpleFiles;
use sdml_core::load::{ModuleLoader, ModuleResolver};
use sdml_core::model::identifiers::Identifier;
use sdml_core::model::modules::HeaderValue;
use sdml_core::model::{HasName, HasSourceSpan};
use sdml_core::stdlib;
use sdml_core::store::ModuleStore;
use sdml_errors::diagnostics::reporter::ReportCounters;
use sdml_errors::diagnostics::SeverityFilter;
use sdml_errors::diagnostics::{functions::imported_module_not_found, StandardStreamReporter};
use sdml_errors::{Diagnostic, Reporter, Source, SourceFiles};
use sdml_errors::{Error, FileId};
use search_path::SearchPath;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use tracing::{debug, error, info, trace, warn};
use url::Url;
#[derive(Clone, Debug)]
pub struct FsModuleResolver {
catalog: Option<ModuleCatalog>,
search_path: SearchPath,
}
pub const SDML_RESOLVER_PATH_VARIABLE: &str = "SDML_PATH";
pub const SDML_FILE_EXTENSION: &str = "sdm";
pub const SDML_FILE_EXTENSION_LONG: &str = "sdml";
pub const SDML_CATALOG_FILE_NAME: &str = "sdml-catalog.json";
#[derive(Debug)]
pub struct FsModuleLoader {
resolver: FsModuleResolver,
module_file_ids: HashMap<Identifier, usize>,
module_files: SourceFiles,
reporter: Box<dyn Reporter>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct ModuleCatalog {
base: Url,
#[serde(skip)]
loaded_from: PathBuf,
entries: HashMap<String, CatalogEntry>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CatalogEntry {
Group(Group),
Item(Item),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct Group {
#[serde(default, skip_serializing_if = "Option::is_none")]
relative_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
relative_path: Option<PathBuf>,
entries: HashMap<String, Item>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct Item {
relative_url: String,
relative_path: PathBuf,
}
macro_rules! trace_entry {
($type_name: literal, $fn_name: literal) => {
const FULL_NAME: &str = concat!($type_name, "::", $fn_name);
let tracing_span = ::tracing::trace_span!(FULL_NAME);
let _enter_span = tracing_span.enter();
::tracing::trace!("{FULL_NAME}()");
};
($type_name: literal, $fn_name: literal => $format: literal, $( $value: expr ),+ ) => {
const FULL_NAME: &str = concat!($type_name, "::", $fn_name);
let tracing_span = ::tracing::trace_span!(FULL_NAME);
let _enter_span = tracing_span.enter();
let arguments = format!($format, $( $value ),+);
::tracing::trace!("{FULL_NAME}({arguments})");
};
}
impl Default for FsModuleResolver {
fn default() -> Self {
trace_entry!("ModuleResolver", "default");
let mut search_path = SearchPath::new_or_default(SDML_RESOLVER_PATH_VARIABLE);
search_path.prepend_cwd();
let catalog = ModuleCatalog::load_from_current(true);
let _self = Self {
catalog,
search_path,
};
trace!("=> {:?}", _self);
_self
}
}
impl ModuleResolver for FsModuleResolver {
fn name_to_resource(&self, name: &Identifier, from: Option<FileId>) -> Result<Url, Error> {
Url::from_file_path(self.name_to_path(name, from)?)
.map_err(|_| Error::UrlParseError { source: None })
}
}
impl FsModuleResolver {
pub fn prepend_to_search_path(&mut self, path: &Path) {
self.search_path.append(PathBuf::from(path));
}
pub fn append_to_search_path(&mut self, path: &Path) {
self.search_path.append(PathBuf::from(path));
}
pub fn name_to_path(&self, name: &Identifier, from: Option<FileId>) -> Result<PathBuf, Error> {
trace_entry!("ModuleResolver", "name_to_path" => "{}", name);
if let Some(catalog) = &self.catalog {
let name: String = name.to_string();
if let Some(path) = catalog.resolve_local_path(&name) {
trace!("Found module in catalog, path: {path:?}");
return Ok(path);
}
}
self.search_path
.find(format!("{}.{}", name, SDML_FILE_EXTENSION).as_ref())
.or_else(|| {
self.search_path
.find(format!("{}/{}.{}", name, name, SDML_FILE_EXTENSION).as_ref())
.or_else(|| {
self.search_path
.find(format!("{}.{}", name, SDML_FILE_EXTENSION_LONG).as_ref())
.or_else(|| {
self.search_path.find(
format!("{}/{}.{}", name, name, SDML_FILE_EXTENSION_LONG)
.as_ref(),
)
})
})
})
.ok_or_else(|| {
imported_module_not_found(
from.unwrap_or_default(),
name.source_span().map(|span| span.into()),
name,
)
.into()
})
}
}
impl Default for FsModuleLoader {
fn default() -> Self {
Self {
resolver: Default::default(),
module_file_ids: Default::default(),
module_files: SimpleFiles::new(),
reporter: Box::<StandardStreamReporter>::default(),
}
}
}
impl ModuleLoader for FsModuleLoader {
fn load(
&mut self,
name: &Identifier,
from: Option<FileId>,
cache: &mut impl ModuleStore,
recursive: bool,
) -> Result<Identifier, Error> {
trace_entry!("ModuleLoader", "load" => "{}", name);
if stdlib::library_module(name).is_some() {
Ok(name.clone())
} else {
let file = match self.resolver.name_to_path(name, from) {
Ok(f) => f,
Err(Error::LanguageValidationError { source }) => {
self.report(&source)?;
return Err(source.into());
}
Err(e) => return Err(e),
};
self.load_from_file(file, cache, recursive)
}
}
fn resolver(&self) -> &impl ModuleResolver {
&self.resolver
}
fn get_file_id(&self, name: &Identifier) -> Option<sdml_errors::FileId> {
self.module_file_ids.get(name).copied()
}
fn get_source(&self, file_id: FileId) -> Option<Source> {
match self.files().get(file_id) {
Ok(file) => Some(file.source().clone()),
Err(err) => {
error!("Could not retrieve module: {file_id:?}, error: {err}");
None
}
}
}
fn report(&self, diagnostic: &Diagnostic) -> Result<(), Error> {
self.reporter.emit(diagnostic, self.files())
}
fn reporter_done(&self, top_module_name: Option<String>) -> Result<ReportCounters, Error> {
self.reporter.done(top_module_name)
}
fn set_severity_filter(&mut self, filter: SeverityFilter) {
self.reporter.set_severity_filter(filter);
}
}
impl FsModuleLoader {
pub fn with_resolver(self, resolver: FsModuleResolver) -> Self {
Self { resolver, ..self }
}
pub fn with_reporter(self, reporter: Box<dyn Reporter>) -> Self {
Self { reporter, ..self }
}
pub fn load_from_file(
&mut self,
file: PathBuf,
cache: &mut impl ModuleStore,
recursive: bool,
) -> Result<Identifier, Error> {
trace_entry!("ModuleLoader", "load_from_file" => "{:?}", file);
let mut reader = File::open(&file)?;
let catalog = self.resolver.catalog.clone();
let module_name = self.load_inner(&mut reader, Some(file.clone()), cache, recursive)?;
let module = cache.get_mut(&module_name).unwrap();
module.set_source_file(file.clone());
if !module.has_base_uri() {
if let Some(catalog) = catalog {
let name = module.name().to_string();
if let Some(url) = catalog.resolve_uri(&name) {
module.set_base_uri(HeaderValue::from(url));
}
} else {
let file = file.canonicalize()?;
match Url::from_file_path(file) {
Ok(base) => module.set_base_uri(HeaderValue::from(base)),
Err(_) => warn!("Could not construct a base URI"),
}
}
}
Ok(module_name)
}
pub fn load_from_reader(
&mut self,
reader: &mut dyn Read,
cache: &mut impl ModuleStore,
recursive: bool,
) -> Result<Identifier, Error> {
trace_entry!("ModuleLoader", "load_from_reader");
self.load_inner(reader, None, cache, recursive)
}
fn load_inner(
&mut self,
reader: &mut dyn Read,
file: Option<PathBuf>,
cache: &mut impl ModuleStore,
recursive: bool,
) -> Result<Identifier, Error> {
trace!("ModuleLoader::load_inner(..., {file:?}, ..., {recursive})");
let mut source = String::new();
reader.read_to_string(&mut source)?;
let file_name: String = file
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let file_id = self.module_files.add(file_name, source.into());
let module = parse_str(file_id, self)?;
let name = module.name().clone();
let _ = self.module_file_ids.insert(name.clone(), file_id);
cache.insert(module);
if recursive {
let dependencies = {
let module = cache.get(&name).unwrap();
module
.imported_modules()
.into_iter()
.cloned()
.collect::<Vec<Identifier>>()
};
for name in &dependencies {
if !cache.contains(name) {
debug!("didn't find module {name} in cache, loading");
self.load(name, Some(file_id), cache, recursive)?;
} else {
debug!("found module {name} in cache");
}
}
}
Ok(name)
}
#[inline(always)]
pub(crate) fn files(&self) -> &SimpleFiles<String, Source> {
&self.module_files
}
}
impl ModuleCatalog {
pub fn load_from_current(look_in_parents: bool) -> Option<Self> {
trace!("ModuleCatalog::load_from_current({look_in_parents})");
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
Self::load_from(&cwd, look_in_parents)
}
pub fn load_from(path: &Path, look_in_parents: bool) -> Option<Self> {
trace!("ModuleCatalog::load_from({path:?}, {look_in_parents})");
if path.is_file() {
Self::load_from_file(path)
} else if path.is_dir() {
let file = path.join(SDML_CATALOG_FILE_NAME);
if file.is_file() {
Self::load_from_file(&file)
} else if look_in_parents {
if let Some(parent_path) = path.parent() {
Self::load_from(parent_path, look_in_parents)
} else {
warn!("No catalog file found in file-system parent path");
None
}
} else {
warn!("No catalog found in provided directory");
None
}
} else {
warn!("The provided path was not a file or directory");
None
}
}
fn load_from_file(file: &Path) -> Option<Self> {
match std::fs::read_to_string(file) {
Ok(source) => match serde_json::from_str::<ModuleCatalog>(&source) {
Ok(mut catalog) => {
catalog.loaded_from = file.parent().unwrap().to_path_buf();
info!("Loaded catalog, file: {file:?}");
Some(catalog)
}
Err(e) => {
error!("Error parsing catalog, file: {file:?}, error: {e}");
None
}
},
Err(e) => {
error!("Error reading catalog, file: {file:?}, error: {e}");
None
}
}
}
pub fn base(&self) -> &Url {
&self.base
}
pub fn set_base(&mut self, base: Url) {
self.base = base;
}
pub fn loaded_from(&self) -> &PathBuf {
&self.loaded_from
}
pub fn has_entries(&self) -> bool {
!self.entries.is_empty()
}
pub fn get_entry(&self, key: &String) -> Option<&CatalogEntry> {
self.entries.get(key)
}
pub fn entries_contains_key(&self, key: &String) -> bool {
self.entries.contains_key(key)
}
pub fn entries(&self) -> impl Iterator<Item = (&String, &CatalogEntry)> {
self.entries.iter()
}
pub fn entry_keys(&self) -> impl Iterator<Item = &String> {
self.entries.keys()
}
pub fn entry_values(&self) -> impl Iterator<Item = &CatalogEntry> {
self.entries.values()
}
pub fn groups(&self) -> impl Iterator<Item = (&String, &Group)> {
self.entries()
.filter_map(|(k, e)| e.as_group().map(|group| (k, group)))
}
pub fn items(&self) -> impl Iterator<Item = (&String, &Item)> {
self.entries()
.filter_map(|(k, e)| e.as_item().map(|item| (k, item)))
}
pub fn resolve_uri(&self, module: &String) -> Option<Url> {
if let Some(CatalogEntry::Item(item)) = self.get_entry(module) {
Some(self.base.join(item.relative_url().as_str()).unwrap())
} else {
self.groups()
.find(|(_, g)| g.entries_contains_key(module))
.map(|(_, g)| g.resolve_uri(&self.base, module))
.unwrap_or_default()
}
}
pub fn resolve_local_path(&self, module: &String) -> Option<PathBuf> {
if let Some(CatalogEntry::Item(item)) = self.get_entry(module) {
Some(self.loaded_from.join(item.relative_path()))
} else {
self.groups()
.find(|(_, g)| g.entries_contains_key(module))
.map(|(_, g)| g.resolve_local_path(&self.loaded_from, module))
.unwrap_or_default()
}
}
}
impl From<Group> for CatalogEntry {
fn from(value: Group) -> Self {
Self::Group(value)
}
}
impl From<Item> for CatalogEntry {
fn from(value: Item) -> Self {
Self::Item(value)
}
}
impl CatalogEntry {
pub fn is_group(&self) -> bool {
matches!(self, Self::Group(_))
}
pub fn as_group(&self) -> Option<&Group> {
match self {
Self::Group(v) => Some(v),
_ => None,
}
}
pub fn is_item(&self) -> bool {
matches!(self, Self::Item(_))
}
pub fn as_item(&self) -> Option<&Item> {
match self {
Self::Item(v) => Some(v),
_ => None,
}
}
}
impl Group {
pub fn relative_path(&self) -> Option<&PathBuf> {
self.relative_path.as_ref()
}
pub fn set_relative_path(&mut self, relative_path: PathBuf) {
self.relative_path = Some(relative_path);
}
pub fn unset_relative_path(&mut self) {
self.relative_path = None;
}
pub fn relative_url(&self) -> Option<&String> {
self.relative_url.as_ref()
}
pub fn set_relative_url(&mut self, relative_url: String) {
self.relative_url = Some(relative_url);
}
pub fn unset_relative_url(&mut self) {
self.relative_url = None;
}
pub fn has_entries(&self) -> bool {
!self.entries.is_empty()
}
pub fn get_entry(&self, key: &String) -> Option<&Item> {
self.entries.get(key)
}
pub fn entries_contains_key(&self, key: &String) -> bool {
self.entries.contains_key(key)
}
pub fn entries(&self) -> impl Iterator<Item = (&String, &Item)> {
self.entries.iter()
}
pub fn entry_keys(&self) -> impl Iterator<Item = &String> {
self.entries.keys()
}
pub fn entry_values(&self) -> impl Iterator<Item = &Item> {
self.entries.values()
}
pub fn resolve_uri(&self, base: &Url, module: &String) -> Option<Url> {
let base = if let Some(relative_url) = &self.relative_url {
base.join(relative_url.as_str()).unwrap()
} else {
base.clone()
};
self.get_entry(module)
.map(|item| base.join(item.relative_url().as_str()).unwrap())
}
pub fn resolve_local_path(&self, base: &Path, module: &String) -> Option<PathBuf> {
let base = if let Some(group_base) = &self.relative_path {
base.join(group_base)
} else {
base.to_path_buf()
};
self.get_entry(module)
.map(|item| base.join(item.relative_url().as_str()))
}
}
impl Item {
pub fn relative_path(&self) -> &PathBuf {
&self.relative_path
}
pub fn set_relative_path(&mut self, relative_path: PathBuf) {
self.relative_path = relative_path;
}
pub fn relative_url(&self) -> &String {
&self.relative_url
}
pub fn set_relative_url(&mut self, relative_url: String) {
self.relative_url = relative_url;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_catalog() {
let catalog = ModuleCatalog {
base: Url::parse("https://example.org/schema/").unwrap(),
loaded_from: PathBuf::from("."),
entries: vec![(
String::from("rentals"),
CatalogEntry::Item(Item {
relative_url: String::from("rentals/v1/"),
relative_path: PathBuf::from("examples/rentals.sdm"),
}),
)]
.into_iter()
.collect(),
};
println!("{}", serde_json::to_string_pretty(&catalog).unwrap());
}
#[test]
fn test_parse_catalog() {
let _: ModuleCatalog = serde_json::from_str(
r#"{
"base": "https://example.org/rentals/",
"entries": {
"vehicle": {
"item": {
"relative_url": "vehicle#",
"relative_path": "vehicle-v1.sdm"
}
}
}
}"#,
)
.unwrap();
}
#[test]
fn test_parse_catalog_with_group() {
let _: ModuleCatalog = serde_json::from_str(
r#"{
"base": "https://example.org/rentals/",
"entries": {
"rentals": {
"group": {
"relative_name": "entities/",
"relative_path": "/entities-v1",
"entries": {
"item": {
"relative_url": "vehicle#",
"relative_path": "vehicle-v1.sdm"
}
}
}
}
}
}"#,
)
.unwrap();
}
}