use std::fmt::Display;
use std::path::Path;
use crate::cli::diagnostic_output::DiagnosticOutput;
use crate::cli::exit_status::ExitStatus;
use crate::cli::unreadable_file::UnreadableFile;
use crate::diagnostic::Diagnostics;
use crate::modules::ModuleSources;
use crate::project::{Project, ProjectReader};
use crate::reader::{Form, Reader};
use crate::source::{SourceId, SourceMap, Span, TextPosition};
pub struct SourceLoader<'a> {
sources: &'a mut SourceMap,
output: &'a dyn DiagnosticOutput,
}
impl<'a> SourceLoader<'a> {
pub fn new(sources: &'a mut SourceMap, output: &'a dyn DiagnosticOutput) -> Self {
Self { sources, output }
}
pub fn load(&mut self, path: &Path) -> Result<SourceId, ExitStatus> {
match ModuleSources::read_file(path) {
Ok(text) => Ok(self.add(path.display(), text)),
Err(error) => Err(UnreadableFile::new(path, &error).report_to(self.output)),
}
}
pub fn load_project(&mut self, path: &Path) -> Result<Project, ExitStatus> {
let source = self.load(path)?;
self.read_project(source)
}
pub fn add(&mut self, name: impl Display, text: impl Into<String>) -> SourceId {
self.sources.add(name.to_string(), text)
}
pub fn add_inside(
&mut self,
enclosing: SourceId,
position: TextPosition,
text: &str,
) -> Option<SourceId> {
self.sources.add_inside(enclosing, position, text)
}
pub fn read(&self, source: SourceId) -> Result<Vec<Form>, ExitStatus> {
Reader::new(self.sources.file(source))
.read()
.map_err(|diagnostics| self.report(&diagnostics))
}
pub fn read_project(&self, source: SourceId) -> Result<Project, ExitStatus> {
let forms = self.read(source)?;
let whole_file = Span::new(source, 0..self.sources.file(source).text().len());
ProjectReader::read(&forms, whole_file).map_err(|diagnostics| self.report(&diagnostics))
}
fn report(&self, diagnostics: &Diagnostics) -> ExitStatus {
self.output.report(self.sources, diagnostics)
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::{Path, PathBuf};
use super::SourceLoader;
use crate::cli::exit_status::ExitStatus;
use crate::cli::recorded_output::RecordedOutput;
use crate::cli::temporary_directory::TemporaryDirectory;
use crate::language::Language;
use crate::source::{SourceId, SourceMap, TextPosition};
#[derive(Default)]
struct LoaderFixture {
sources: SourceMap,
output: RecordedOutput,
}
impl LoaderFixture {
const MODULE: &str = "geometry.knv";
const MISSING: &str = "no-such-directory/missing.knv";
fn loader(&mut self) -> SourceLoader<'_> {
SourceLoader::new(&mut self.sources, &self.output)
}
fn added(&mut self, text: &str) -> SourceId {
self.loader().add(Self::MODULE, text.to_string())
}
fn written_project_file(directory: &TemporaryDirectory, text: &str) -> PathBuf {
let path = directory.path().join(Language::project_file_name());
fs::write(&path, text).expect("the scratch directory is writable");
path
}
fn reports(&self) -> Vec<String> {
self.output.writes()
}
}
#[test]
fn reads_the_forms_of_an_added_text() {
let mut fixture = LoaderFixture::default();
let source = fixture.added("(area 2 3) (area 4 5)");
let forms = fixture.loader().read(source).expect("the text reads");
assert_eq!(forms.len(), 2);
}
#[test]
fn names_an_added_text_by_its_path() {
let mut fixture = LoaderFixture::default();
let source = fixture.added("(area 2 3)");
assert_eq!(fixture.sources.file(source).name(), LoaderFixture::MODULE);
}
#[test]
fn names_a_named_text_by_its_name() {
let mut fixture = LoaderFixture::default();
let source = fixture.loader().add("<input 1>", "(area 2 3)");
assert_eq!(fixture.sources.file(source).name(), "<input 1>");
}
#[test]
fn reports_a_text_that_does_not_read() {
let mut fixture = LoaderFixture::default();
let source = fixture.added("(area 2 3");
let status = fixture.loader().read(source);
assert_eq!(status.err(), Some(ExitStatus::ProgramErrors));
assert_eq!(fixture.reports(), ["1 diagnostics"]);
}
#[test]
fn reports_a_missing_file_through_its_output() {
let mut fixture = LoaderFixture::default();
let status = fixture.loader().load(Path::new(LoaderFixture::MISSING));
assert_eq!(status.err(), Some(ExitStatus::UsageError));
let [report] = fixture.reports().try_into().expect("one report");
assert!(report.starts_with("error: cannot read `no-such-directory/missing.knv`: "));
}
#[test]
fn loads_a_project_from_its_file() {
let directory = TemporaryDirectory::create().expect("a scratch directory is created");
let path = LoaderFixture::written_project_file(&directory, "(defproject app)");
let mut fixture = LoaderFixture::default();
let project = fixture
.loader()
.load_project(&path)
.expect("the project loads");
assert_eq!(project.name, "app");
}
#[test]
fn loads_no_project_from_a_missing_file() {
let mut fixture = LoaderFixture::default();
let status = fixture
.loader()
.load_project(Path::new(LoaderFixture::MISSING));
assert_eq!(status.err(), Some(ExitStatus::UsageError));
assert_eq!(fixture.reports().len(), 1);
}
#[test]
fn adds_an_excerpt_found_at_its_position_under_its_module_name() {
let mut fixture = LoaderFixture::default();
let module = fixture.added("(area 2 3)\n(area 4 5)\n");
let excerpt = fixture
.loader()
.add_inside(module, TextPosition::new(2, 1), "(area 4 5)")
.expect("the excerpt is at its position");
assert_eq!(fixture.sources.file(excerpt).name(), LoaderFixture::MODULE);
}
#[test]
fn refuses_an_excerpt_missing_from_its_position() {
let mut fixture = LoaderFixture::default();
let module = fixture.added("(area 2 3)\n");
let excerpt = fixture
.loader()
.add_inside(module, TextPosition::new(1, 1), "(area 4 5)");
assert!(excerpt.is_none());
}
#[test]
fn reads_a_project_from_its_project_form() {
let mut fixture = LoaderFixture::default();
let source = fixture.added("(defproject app)");
let project = fixture
.loader()
.read_project(source)
.expect("the project reads");
assert_eq!(project.name, "app");
}
#[test]
fn reports_a_project_file_without_a_project_form() {
let mut fixture = LoaderFixture::default();
let source = fixture.added("(area 2 3)");
let status = fixture.loader().read_project(source);
assert_eq!(status.err(), Some(ExitStatus::ProgramErrors));
assert_eq!(fixture.reports(), ["1 diagnostics"]);
}
}