tinymist_project/
model.rs1use std::hash::Hash;
2use std::path::{Path, PathBuf};
3
4use ecow::EcoVec;
5use tinymist_std::error::prelude::*;
6use tinymist_std::{ImmutPath, bail};
7use typst::diag::EcoString;
8
9pub use task::*;
10pub use tinymist_task as task;
11
12pub const LOCK_VERSION: &str = "0.1.0-beta0";
14
15#[derive(Debug, serde::Serialize, serde::Deserialize)]
17#[serde(rename_all = "kebab-case", tag = "version")]
18pub enum LockFileCompat {
19 #[serde(rename = "0.1.0-beta0")]
21 Version010Beta0(LockFile),
22 #[serde(untagged)]
24 Other(serde_json::Value),
25}
26
27impl LockFileCompat {
28 pub fn version(&self) -> Result<&str> {
30 match self {
31 LockFileCompat::Version010Beta0(..) => Ok(LOCK_VERSION),
32 LockFileCompat::Other(v) => v
33 .get("version")
34 .and_then(|v| v.as_str())
35 .context("missing version field"),
36 }
37 }
38
39 pub fn migrate(self) -> Result<LockFile> {
41 match self {
42 LockFileCompat::Version010Beta0(v) => Ok(v),
43 this @ LockFileCompat::Other(..) => {
44 bail!(
45 "cannot migrate from version: {}",
46 this.version().unwrap_or("unknown version")
47 )
48 }
49 }
50 }
51}
52
53#[derive(Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
55pub struct LockFile {
56 #[serde(skip)]
58 pub lock_dir: Option<ImmutPath>,
59 #[serde(skip_serializing_if = "Vec::is_empty", default)]
63 pub document: Vec<ProjectInput>,
64 #[serde(skip_serializing_if = "Vec::is_empty", default)]
66 pub task: Vec<ApplyProjectTask>,
67 #[serde(skip_serializing_if = "EcoVec::is_empty", default)]
69 pub route: EcoVec<ProjectRoute>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
74#[serde(rename_all = "kebab-case")]
75pub struct ProjectInput {
76 pub id: Id,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub lock_dir: Option<PathBuf>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub root: Option<ResourcePath>,
84 pub main: ResourcePath,
86 pub inputs: Vec<(String, String)>,
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub font_paths: Vec<ResourcePath>,
91 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
93 pub system_fonts: bool,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub package_path: Option<ResourcePath>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub package_cache_path: Option<ResourcePath>,
100}
101
102impl ProjectInput {
103 pub fn relative_to(&self, that: &Path) -> Self {
105 if let Some(lock_dir) = &self.lock_dir
106 && lock_dir == that
107 {
108 return self.clone();
109 }
110
111 todo!()
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
117#[serde(rename_all = "kebab-case")]
118pub struct ProjectMaterial {
119 pub root: EcoString,
121 pub id: Id,
123 pub files: Vec<ResourcePath>,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
129#[serde(rename_all = "kebab-case")]
130pub struct ProjectPathMaterial {
131 pub root: EcoString,
133 pub id: Id,
135 pub files: Vec<PathBuf>,
137}
138
139impl ProjectPathMaterial {
140 pub fn from_deps(doc_id: Id, files: EcoVec<ImmutPath>) -> Self {
143 let mut files: Vec<_> = files.into_iter().map(|p| p.as_ref().to_owned()).collect();
144 files.sort();
145
146 ProjectPathMaterial {
147 root: EcoString::default(),
148 id: doc_id,
149 files,
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
156#[serde(rename_all = "kebab-case")]
157pub struct ProjectRoute {
158 pub id: Id,
160 pub priority: u32,
162}
163
164#[cfg(test)]
165mod tests {
166 use std::path::Path;
167
168 use tinymist_task::PathPattern;
169 use tinymist_world::EntryState;
170 use typst::syntax::VirtualPath;
171
172 use super::*;
173
174 #[test]
175 fn test_substitute_path() {
176 let root = Path::new("/dummy-root");
177 let entry =
178 EntryState::new_rooted(root.into(), Some(VirtualPath::new("/dir1/dir2/file.txt")));
179
180 assert_eq!(
181 PathPattern::new("/substitute/$dir/$name").substitute(&entry),
182 Some(PathBuf::from("/substitute/dir1/dir2/file.txt").into())
183 );
184 assert_eq!(
185 PathPattern::new("/substitute/$dir/../$name").substitute(&entry),
186 Some(PathBuf::from("/substitute/dir1/file.txt").into())
187 );
188 assert_eq!(
189 PathPattern::new("/substitute/$name").substitute(&entry),
190 Some(PathBuf::from("/substitute/file.txt").into())
191 );
192 assert_eq!(
193 PathPattern::new("/substitute/target/$dir/$name").substitute(&entry),
194 Some(PathBuf::from("/substitute/target/dir1/dir2/file.txt").into())
195 );
196 }
197}