tinymist_project/
model.rs

1use 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
12/// The currently using lock file version.
13pub const LOCK_VERSION: &str = "0.1.0-beta0";
14
15/// A lock file compatibility wrapper.
16#[derive(Debug, serde::Serialize, serde::Deserialize)]
17#[serde(rename_all = "kebab-case", tag = "version")]
18pub enum LockFileCompat {
19    /// The lock file schema with version 0.1.0-beta0.
20    #[serde(rename = "0.1.0-beta0")]
21    Version010Beta0(LockFile),
22    /// Other lock file schema.
23    #[serde(untagged)]
24    Other(serde_json::Value),
25}
26
27impl LockFileCompat {
28    /// Returns the lock file version.
29    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    /// Migrates the lock file to the current version.
40    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/// A lock file storing project information.
54#[derive(Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
55pub struct LockFile {
56    /// The directory where stores the lock file.
57    #[serde(skip)]
58    pub lock_dir: Option<ImmutPath>,
59    // The lock file version.
60    // version: String,
61    /// The project's document (input).
62    #[serde(skip_serializing_if = "Vec::is_empty", default)]
63    pub document: Vec<ProjectInput>,
64    /// The project's task (output).
65    #[serde(skip_serializing_if = "Vec::is_empty", default)]
66    pub task: Vec<ApplyProjectTask>,
67    /// The project's task route.
68    #[serde(skip_serializing_if = "EcoVec::is_empty", default)]
69    pub route: EcoVec<ProjectRoute>,
70}
71
72/// A project input specifier.
73#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
74#[serde(rename_all = "kebab-case")]
75pub struct ProjectInput {
76    /// The project's ID.
77    pub id: Id,
78    /// The cwd of the project when relative paths will be resolved.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub lock_dir: Option<PathBuf>,
81    /// The path to the root directory of the project.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub root: Option<ResourcePath>,
84    /// The path to the main file of the project.
85    pub main: ResourcePath,
86    /// The key-value pairs visible through `sys.inputs`
87    pub inputs: Vec<(String, String)>,
88    /// The project's font paths.
89    #[serde(default, skip_serializing_if = "Vec::is_empty")]
90    pub font_paths: Vec<ResourcePath>,
91    /// Whether to use system fonts.
92    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
93    pub system_fonts: bool,
94    /// The project's package path.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub package_path: Option<ResourcePath>,
97    /// The project's package cache path.
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub package_cache_path: Option<ResourcePath>,
100}
101
102impl ProjectInput {
103    /// Returns a new project input relative to the provided lock directory.
104    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/// A project route specifier.
116#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
117#[serde(rename_all = "kebab-case")]
118pub struct ProjectMaterial {
119    /// The root of the project that the material belongs to.
120    pub root: EcoString,
121    /// A project.
122    pub id: Id,
123    /// The files.
124    pub files: Vec<ResourcePath>,
125}
126
127/// A project route specifier.
128#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
129#[serde(rename_all = "kebab-case")]
130pub struct ProjectPathMaterial {
131    /// The root of the project that the material belongs to.
132    pub root: EcoString,
133    /// A project.
134    pub id: Id,
135    /// The files.
136    pub files: Vec<PathBuf>,
137}
138
139impl ProjectPathMaterial {
140    /// Creates a new project path material from a document ID and a list of
141    /// files.
142    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/// A project route specifier.
155#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
156#[serde(rename_all = "kebab-case")]
157pub struct ProjectRoute {
158    /// A project.
159    pub id: Id,
160    /// The priority of the project. (lower numbers are higher priority).
161    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}