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://mosaiclang.dev/assets/A4.svg",
8    html_favicon_url = "https://mosaiclang.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    /// # Errors
150    ///
151    /// Returns [`ManifestError::Io`] when the file cannot be read and
152    /// [`ManifestError::Parse`] when TOML decoding fails.
153    ///
154    /// # Examples
155    ///
156    /// ```no_run
157    /// use std::path::Path;
158    ///
159    /// use mos_packages::ProjectManifest;
160    ///
161    /// let manifest = ProjectManifest::load(Path::new("mosaic.toml"))?;
162    ///
163    /// assert!(!manifest.project.entry.is_empty());
164    /// # Ok::<(), mos_packages::ManifestError>(())
165    /// ```
166    pub fn load(path: &Path) -> Result<Self, ManifestError> {
167        let text = std::fs::read_to_string(path).map_err(|source| ManifestError::Io {
168            path: path.to_path_buf(),
169            source,
170        })?;
171        toml::from_str(&text).map_err(|source| ManifestError::Parse {
172            path: path.to_path_buf(),
173            source,
174        })
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn parses_optional_pdf_output_path() {
184        let manifest: ProjectManifest = toml::from_str(
185            r#"
186            [project]
187            name = "demo"
188            version = "0.1.0"
189            entry = "main.mos"
190
191            [output]
192            pdf = "demo.pdf"
193            "#,
194        )
195        .expect("manifest parses");
196
197        assert_eq!(manifest.output.pdf.as_deref(), Some("demo.pdf"));
198    }
199}