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}