use std::path::{Path, PathBuf};
use orrery_core::identifier::Id;
use crate::error::SourceError;
pub trait SourceProvider {
fn resolve_path(&self, from: &Path, import_path: &str) -> Result<PathBuf, SourceError>;
fn read_source(&self, path: &Path) -> Result<String, SourceError>;
fn derive_namespace(&self, import_path: &Path) -> Result<Id, SourceError> {
let name = import_path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| {
SourceError::new(
import_path,
"cannot derive namespace: path has no valid file stem",
)
})?;
Ok(Id::new(name))
}
}
impl<P: SourceProvider> SourceProvider for &P {
fn resolve_path(&self, from: &Path, import_path: &str) -> Result<PathBuf, SourceError> {
(**self).resolve_path(from, import_path)
}
fn read_source(&self, path: &Path) -> Result<String, SourceError> {
(**self).read_source(path)
}
fn derive_namespace(&self, import_path: &Path) -> Result<Id, SourceError> {
(**self).derive_namespace(import_path)
}
}
#[derive(Debug, Clone, Default)]
pub struct InMemorySourceProvider {
files: std::collections::HashMap<PathBuf, String>,
}
impl InMemorySourceProvider {
pub fn new() -> Self {
Self::default()
}
pub fn add_file(&mut self, path: impl Into<PathBuf>, source: impl Into<String>) {
self.files.insert(path.into(), source.into());
}
}
impl SourceProvider for InMemorySourceProvider {
fn resolve_path(&self, from: &Path, import_path: &str) -> Result<PathBuf, SourceError> {
let dir = from.parent().unwrap_or_else(|| Path::new(""));
let mut target = dir.join(import_path);
target.set_extension("orr");
if self.files.contains_key(&target) {
Ok(target)
} else {
Err(SourceError::new(
&target,
format!("file not found: {}", target.display()),
))
}
}
fn read_source(&self, path: &Path) -> Result<String, SourceError> {
self.files
.get(path)
.cloned()
.ok_or_else(|| SourceError::new(path, format!("file not found: {}", path.display())))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_path_same_directory() {
let mut provider = InMemorySourceProvider::new();
provider.add_file("styles.orr", "library;");
let resolved = provider
.resolve_path(Path::new("main.orr"), "styles")
.unwrap();
assert_eq!(resolved, PathBuf::from("styles.orr"));
}
#[test]
fn resolve_path_subdirectory() {
let mut provider = InMemorySourceProvider::new();
provider.add_file("shared/styles.orr", "library;");
let resolved = provider
.resolve_path(Path::new("main.orr"), "shared/styles")
.unwrap();
assert_eq!(resolved, PathBuf::from("shared/styles.orr"));
}
#[test]
fn resolve_path_from_nested_file() {
let mut provider = InMemorySourceProvider::new();
provider.add_file("shared/base.orr", "library;");
let resolved = provider
.resolve_path(Path::new("shared/ext.orr"), "base")
.unwrap();
assert_eq!(resolved, PathBuf::from("shared/base.orr"));
}
#[test]
fn resolve_path_file_not_found() {
let provider = InMemorySourceProvider::new();
let err = provider
.resolve_path(Path::new("main.orr"), "missing")
.unwrap_err();
assert_eq!(err.path(), Path::new("missing.orr"));
assert!(err.message().contains("file not found"));
}
#[test]
fn resolve_path_appends_orr_extension() {
let mut provider = InMemorySourceProvider::new();
provider.add_file("lib.orr", "library;");
let resolved = provider.resolve_path(Path::new("main.orr"), "lib").unwrap();
assert_eq!(resolved, PathBuf::from("lib.orr"));
}
#[test]
fn read_source_existing_file() {
let mut provider = InMemorySourceProvider::new();
provider.add_file("styles.orr", "library;\ntype Box = Rectangle;");
let source = provider.read_source(Path::new("styles.orr")).unwrap();
assert_eq!(source, "library;\ntype Box = Rectangle;");
}
#[test]
fn read_source_missing_file() {
let provider = InMemorySourceProvider::new();
let err = provider.read_source(Path::new("missing.orr")).unwrap_err();
assert_eq!(err.path(), Path::new("missing.orr"));
assert!(err.message().contains("file not found"));
}
#[test]
fn resolve_then_read_round_trip() {
let mut provider = InMemorySourceProvider::new();
provider.add_file("shared/styles.orr", "library;\ntype S = Rectangle;");
provider.add_file("main.orr", "diagram component;");
let resolved = provider
.resolve_path(Path::new("main.orr"), "shared/styles")
.unwrap();
let source = provider.read_source(&resolved).unwrap();
assert!(source.contains("type S = Rectangle"));
}
#[test]
fn resolve_chained_imports() {
let mut provider = InMemorySourceProvider::new();
provider.add_file("base.orr", "library;");
provider.add_file("ext.orr", "library;");
provider.add_file("main.orr", "diagram component;");
let ext_path = provider.resolve_path(Path::new("main.orr"), "ext").unwrap();
assert_eq!(ext_path, PathBuf::from("ext.orr"));
let base_path = provider.resolve_path(&ext_path, "base").unwrap();
assert_eq!(base_path, PathBuf::from("base.orr"));
let base_source = provider.read_source(&base_path).unwrap();
assert_eq!(base_source, "library;");
}
#[test]
fn resolve_nested_directory_structure() {
let mut provider = InMemorySourceProvider::new();
provider.add_file("shared/base/types.orr", "library;");
provider.add_file("shared/ext.orr", "library;");
let types = provider
.resolve_path(Path::new("shared/ext.orr"), "base/types")
.unwrap();
assert_eq!(types, PathBuf::from("shared/base/types.orr"));
}
#[test]
fn empty_import_path_is_error() {
let provider = InMemorySourceProvider::new();
let err = provider
.resolve_path(Path::new("main.orr"), "")
.unwrap_err();
assert!(err.message().contains("file not found"));
}
#[test]
fn derive_namespace() {
let provider = InMemorySourceProvider::new();
let id = provider.derive_namespace(Path::new("simple")).unwrap();
assert!(id == "simple");
let id = provider
.derive_namespace(Path::new("shared/nested"))
.unwrap();
assert!(id == "nested");
let id = provider
.derive_namespace(Path::new("../relative/path"))
.unwrap();
assert!(id == "path");
let id = provider
.derive_namespace(Path::new("shared/extension.orr"))
.unwrap();
assert!(id == "extension");
}
#[test]
fn derive_namespace_empty_path_is_error() {
let provider = InMemorySourceProvider::new();
let err = provider.derive_namespace(Path::new("")).unwrap_err();
assert!(
err.message().contains("cannot derive namespace"),
"expected namespace error, got: {}",
err.message()
);
}
#[test]
fn overwrite_file() {
let mut provider = InMemorySourceProvider::new();
provider.add_file("a.orr", "version 1");
provider.add_file("a.orr", "version 2");
let source = provider.read_source(Path::new("a.orr")).unwrap();
assert_eq!(source, "version 2");
}
}