typst_pack/
project_snapshot.rs1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5
6use crate::pack::Pack;
7use crate::payload::SharedBytes;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct ProjectSnapshot {
13 entrypoint: String,
14 files: BTreeMap<String, SharedBytes>,
15}
16
17impl ProjectSnapshot {
18 pub fn entrypoint(&self) -> &str {
20 &self.entrypoint
21 }
22
23 pub fn files(&self) -> impl Iterator<Item = (&str, &[u8])> {
25 self.files
26 .iter()
27 .map(|(path, data)| (path.as_str(), data.as_slice()))
28 }
29
30 pub fn file(&self, path: &str) -> Option<&[u8]> {
32 self.files.get(path).map(SharedBytes::as_slice)
33 }
34
35 pub(crate) fn shared_files(&self) -> impl Iterator<Item = (&str, &SharedBytes)> {
36 self.files.iter().map(|(path, data)| (path.as_str(), data))
37 }
38
39 pub(crate) fn shared_file(&self, path: &str) -> Option<&SharedBytes> {
40 self.files.get(path)
41 }
42}
43
44#[derive(Debug)]
50pub struct ProjectSnapshotAssembly {
51 entrypoint: String,
52}
53
54impl ProjectSnapshotAssembly {
55 pub fn new(entrypoint: impl Into<String>) -> Self {
57 Self {
58 entrypoint: entrypoint.into(),
59 }
60 }
61
62 pub fn assemble(
64 &self,
65 entries: impl IntoIterator<Item = (impl AsRef<str>, impl Into<Vec<u8>>)>,
66 ) -> Result<ProjectSnapshot, ProjectSnapshotError> {
67 let mut issues = Vec::new();
68 let entrypoint = match canonical_project_path(&self.entrypoint) {
69 Ok(path) => Some(path),
70 Err(issue) => {
71 issues.push(issue);
72 None
73 }
74 };
75
76 let mut selected_entries = Vec::new();
77 for (path, data) in entries {
78 match canonical_project_path(path.as_ref()) {
79 Ok(path) => selected_entries.push((path, SharedBytes::new(data.into()))),
80 Err(issue) => issues.push(issue),
81 }
82 }
83
84 let mut paths = BTreeSet::new();
85 let mut duplicate_paths = BTreeSet::new();
86 for (path, _) in &selected_entries {
87 if !paths.insert(path.as_str()) {
88 duplicate_paths.insert(path.clone());
89 }
90 }
91 issues.extend(
92 duplicate_paths
93 .into_iter()
94 .map(|path| ProjectSnapshotIssue::DuplicatePath { path }),
95 );
96
97 if let Some(entrypoint) = &entrypoint
98 && !paths.contains(entrypoint.as_str())
99 {
100 issues.push(ProjectSnapshotIssue::MissingEntrypoint {
101 path: entrypoint.clone(),
102 });
103 }
104 issues.sort_by(|left, right| left.sort_key().cmp(&right.sort_key()));
105 if !issues.is_empty() {
106 return Err(ProjectSnapshotError { issues });
107 }
108 let files = selected_entries.into_iter().collect::<BTreeMap<_, _>>();
109 Ok(ProjectSnapshot {
110 entrypoint: entrypoint.expect("a valid Project Snapshot has a canonical entrypoint"),
111 files,
112 })
113 }
114}
115
116fn canonical_project_path(path: &str) -> Result<String, ProjectSnapshotIssue> {
118 Pack::canonical_project_path(path).map_err(|message| ProjectSnapshotIssue::InvalidPath {
119 path: path.to_owned(),
120 message,
121 })
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
126#[non_exhaustive]
127pub enum ProjectSnapshotIssue {
128 #[error("project path {path:?} cannot be represented: {message}")]
130 InvalidPath { path: String, message: String },
131 #[error("project path {path:?} is supplied more than once")]
133 DuplicatePath { path: String },
134 #[error("entrypoint {path:?} is not a supplied project file")]
136 MissingEntrypoint { path: String },
137}
138
139impl ProjectSnapshotIssue {
140 fn sort_key(&self) -> (&str, u8, &str) {
141 match self {
142 Self::InvalidPath { path, message } => (path, 0, message),
143 Self::DuplicatePath { path } => (path, 1, ""),
144 Self::MissingEntrypoint { path } => (path, 2, ""),
145 }
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct ProjectSnapshotError {
152 issues: Vec<ProjectSnapshotIssue>,
153}
154
155impl ProjectSnapshotError {
156 pub fn issues(&self) -> &[ProjectSnapshotIssue] {
158 &self.issues
159 }
160}
161
162impl fmt::Display for ProjectSnapshotError {
163 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164 if let [issue] = self.issues.as_slice() {
165 return issue.fmt(formatter);
166 }
167 write!(
168 formatter,
169 "Project Snapshot assembly failed with {} issue(s)",
170 self.issues.len()
171 )?;
172 for issue in &self.issues {
173 write!(formatter, ": {issue}")?;
174 }
175 Ok(())
176 }
177}
178
179impl std::error::Error for ProjectSnapshotError {}