use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use crate::pack::Pack;
use crate::payload::SharedBytes;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProjectSnapshot {
entrypoint: String,
files: BTreeMap<String, SharedBytes>,
}
impl ProjectSnapshot {
pub fn entrypoint(&self) -> &str {
&self.entrypoint
}
pub fn files(&self) -> impl Iterator<Item = (&str, &[u8])> {
self.files
.iter()
.map(|(path, data)| (path.as_str(), data.as_slice()))
}
pub fn file(&self, path: &str) -> Option<&[u8]> {
self.files.get(path).map(SharedBytes::as_slice)
}
pub(crate) fn shared_files(&self) -> impl Iterator<Item = (&str, &SharedBytes)> {
self.files.iter().map(|(path, data)| (path.as_str(), data))
}
pub(crate) fn shared_file(&self, path: &str) -> Option<&SharedBytes> {
self.files.get(path)
}
}
#[derive(Debug)]
pub struct ProjectSnapshotAssembly {
entrypoint: String,
}
impl ProjectSnapshotAssembly {
pub fn new(entrypoint: impl Into<String>) -> Self {
Self {
entrypoint: entrypoint.into(),
}
}
pub fn assemble(
&self,
entries: impl IntoIterator<Item = (impl AsRef<str>, impl Into<Vec<u8>>)>,
) -> Result<ProjectSnapshot, ProjectSnapshotError> {
let mut issues = Vec::new();
let entrypoint = match canonical_project_path(&self.entrypoint) {
Ok(path) => Some(path),
Err(issue) => {
issues.push(issue);
None
}
};
let mut selected_entries = Vec::new();
for (path, data) in entries {
match canonical_project_path(path.as_ref()) {
Ok(path) => selected_entries.push((path, SharedBytes::new(data.into()))),
Err(issue) => issues.push(issue),
}
}
let mut paths = BTreeSet::new();
let mut duplicate_paths = BTreeSet::new();
for (path, _) in &selected_entries {
if !paths.insert(path.as_str()) {
duplicate_paths.insert(path.clone());
}
}
issues.extend(
duplicate_paths
.into_iter()
.map(|path| ProjectSnapshotIssue::DuplicatePath { path }),
);
if let Some(entrypoint) = &entrypoint
&& !paths.contains(entrypoint.as_str())
{
issues.push(ProjectSnapshotIssue::MissingEntrypoint {
path: entrypoint.clone(),
});
}
issues.sort_by(|left, right| left.sort_key().cmp(&right.sort_key()));
if !issues.is_empty() {
return Err(ProjectSnapshotError { issues });
}
let files = selected_entries.into_iter().collect::<BTreeMap<_, _>>();
Ok(ProjectSnapshot {
entrypoint: entrypoint.expect("a valid Project Snapshot has a canonical entrypoint"),
files,
})
}
}
fn canonical_project_path(path: &str) -> Result<String, ProjectSnapshotIssue> {
Pack::canonical_project_path(path).map_err(|message| ProjectSnapshotIssue::InvalidPath {
path: path.to_owned(),
message,
})
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ProjectSnapshotIssue {
#[error("project path {path:?} cannot be represented: {message}")]
InvalidPath { path: String, message: String },
#[error("project path {path:?} is supplied more than once")]
DuplicatePath { path: String },
#[error("entrypoint {path:?} is not a supplied project file")]
MissingEntrypoint { path: String },
}
impl ProjectSnapshotIssue {
fn sort_key(&self) -> (&str, u8, &str) {
match self {
Self::InvalidPath { path, message } => (path, 0, message),
Self::DuplicatePath { path } => (path, 1, ""),
Self::MissingEntrypoint { path } => (path, 2, ""),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectSnapshotError {
issues: Vec<ProjectSnapshotIssue>,
}
impl ProjectSnapshotError {
pub fn issues(&self) -> &[ProjectSnapshotIssue] {
&self.issues
}
}
impl fmt::Display for ProjectSnapshotError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let [issue] = self.issues.as_slice() {
return issue.fmt(formatter);
}
write!(
formatter,
"Project Snapshot assembly failed with {} issue(s)",
self.issues.len()
)?;
for issue in &self.issues {
write!(formatter, ": {issue}")?;
}
Ok(())
}
}
impl std::error::Error for ProjectSnapshotError {}