Skip to main content

mos_packages/
lib.rs

1//! Project / package manifest for Mosaic (`mosaic.toml`, manifest ยง14).
2//!
3//! Real dependency resolution and lockfile generation land later.
4//! For now this crate only defines the manifest schema and parses it.
5
6#![doc(
7    html_logo_url = "https://mosaic.kjanat.dev/assets/A4.svg",
8    html_favicon_url = "https://mosaic.kjanat.dev/assets/A4.svg"
9)]
10
11use std::collections::BTreeMap;
12use std::path::{Path, PathBuf};
13
14use serde::{Deserialize, Serialize};
15
16/// Parsed `mosaic.toml` project manifest.
17///
18/// # Examples
19///
20/// ```
21/// use mos_packages::ProjectManifest;
22///
23/// let manifest: ProjectManifest = toml::from_str(
24///     r#"
25///     [project]
26///     name = "demo"
27///     version = "0.1.0"
28///     entry = "main.mos"
29///     "#,
30/// )?;
31///
32/// assert_eq!(manifest.project.name, "demo");
33/// # Ok::<(), toml::de::Error>(())
34/// ```
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct ProjectManifest {
38    pub project: ProjectSection,
39
40    #[serde(default)]
41    pub document: DocumentSection,
42
43    #[serde(default)]
44    pub output: OutputSection,
45
46    #[serde(default)]
47    pub dependencies: BTreeMap<String, String>,
48}
49
50/// Required `[project]` section of `mosaic.toml`.
51///
52/// # Examples
53///
54/// ```
55/// use mos_packages::ProjectSection;
56///
57/// let project = ProjectSection {
58///     name: "demo".to_owned(),
59///     version: "0.1.0".to_owned(),
60///     entry: "main.mos".to_owned(),
61/// };
62///
63/// assert_eq!(project.entry, "main.mos");
64/// ```
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct ProjectSection {
68    pub name: String,
69    pub version: String,
70    pub entry: String,
71}
72
73/// Optional `[document]` defaults from `mosaic.toml`.
74///
75/// # Examples
76///
77/// ```
78/// use mos_packages::DocumentSection;
79///
80/// let section = DocumentSection::default();
81///
82/// assert!(section.output.is_empty());
83/// ```
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85#[serde(deny_unknown_fields)]
86pub struct DocumentSection {
87    #[serde(default)]
88    pub language: Option<String>,
89
90    #[serde(default)]
91    pub output: Vec<String>,
92}
93
94/// Declared output paths from `mosaic.toml`.
95///
96/// Paths are interpreted by the CLI relative to the project directory.
97///
98/// # Examples
99///
100/// ```
101/// use mos_packages::OutputSection;
102///
103/// let section = OutputSection {
104///     pdf: Some("paper.pdf".to_owned()),
105/// };
106///
107/// assert_eq!(section.pdf.as_deref(), Some("paper.pdf"));
108/// ```
109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct OutputSection {
112    #[serde(default)]
113    pub pdf: Option<String>,
114}
115
116/// Error returned when loading a manifest from disk.
117///
118/// # Examples
119///
120/// ```
121/// use std::path::Path;
122///
123/// use mos_packages::{ManifestError, ProjectManifest};
124///
125/// let err = ProjectManifest::load(Path::new("definitely-missing-mosaic.toml")).err();
126///
127/// assert!(matches!(err, Some(ManifestError::Io { .. })));
128/// ```
129#[derive(thiserror::Error, Debug)]
130pub enum ManifestError {
131    #[error("could not read manifest `{}`: {}", mos_core::display_path(.path), .source)]
132    Io {
133        path: PathBuf,
134        #[source]
135        source: std::io::Error,
136    },
137
138    #[error("could not parse manifest `{}`: {}", mos_core::display_path(.path), .source)]
139    Parse {
140        path: PathBuf,
141        #[source]
142        source: toml::de::Error,
143    },
144}
145
146impl ProjectManifest {
147    /// Load and parse a `mosaic.toml` from disk.
148    ///
149    /// # Examples
150    ///
151    /// ```no_run
152    /// use std::path::Path;
153    ///
154    /// use mos_packages::ProjectManifest;
155    ///
156    /// let manifest = ProjectManifest::load(Path::new("mosaic.toml"))?;
157    ///
158    /// assert!(!manifest.project.entry.is_empty());
159    /// # Ok::<(), mos_packages::ManifestError>(())
160    /// ```
161    pub fn load(path: &Path) -> Result<Self, ManifestError> {
162        let text = std::fs::read_to_string(path).map_err(|source| ManifestError::Io {
163            path: path.to_path_buf(),
164            source,
165        })?;
166        toml::from_str(&text).map_err(|source| ManifestError::Parse {
167            path: path.to_path_buf(),
168            source,
169        })
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn parses_optional_pdf_output_path() {
179        let manifest: ProjectManifest = toml::from_str(
180            r#"
181            [project]
182            name = "demo"
183            version = "0.1.0"
184            entry = "main.mos"
185
186            [output]
187            pdf = "demo.pdf"
188            "#,
189        )
190        .expect("manifest parses");
191
192        assert_eq!(manifest.output.pdf.as_deref(), Some("demo.pdf"));
193    }
194}