wdl_modules/project/
document.rs1use thiserror::Error;
2
3use crate::Manifest;
4use crate::dependency::DependencyName;
5use crate::dependency::DependencySource;
6use crate::manifest::ManifestError;
7
8#[derive(Debug, Error)]
10pub enum ManifestDocumentError {
11 #[error("invalid module manifest")]
13 Manifest(#[from] ManifestError),
14
15 #[error("invalid manifest JSON")]
17 Json(#[from] serde_json::Error),
18
19 #[error("`module.json` root must be an object")]
21 RootNotObject,
22
23 #[error("`dependencies` in `module.json` must be an object")]
25 DependenciesNotObject,
26}
27
28#[derive(Clone, Debug)]
71pub struct ManifestDocument {
72 value: serde_json::Value,
74 manifest: Manifest,
76}
77
78impl ManifestDocument {
79 pub fn parse(bytes: &[u8]) -> Result<Self, ManifestDocumentError> {
82 let manifest = Manifest::parse(bytes)?;
83 let value = serde_json::from_slice(bytes)?;
84 Ok(Self { value, manifest })
85 }
86
87 pub fn manifest(&self) -> &Manifest {
90 &self.manifest
91 }
92
93 pub fn insert_dependency(
100 &mut self,
101 name: &str,
102 source: &DependencySource,
103 ) -> Result<(), ManifestDocumentError> {
104 let name = Self::parse_dependency_name(name)?;
105 let serialized = serde_json::to_value(source)?;
106 let superseded = self
112 .matching_dependency_key(&name)
113 .filter(|existing| *existing != name.manifest())
114 .map(str::to_string);
115 let root = self
116 .value
117 .as_object_mut()
118 .ok_or(ManifestDocumentError::RootNotObject)?;
119 let dependencies = root
120 .entry("dependencies")
121 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()))
122 .as_object_mut()
123 .ok_or(ManifestDocumentError::DependenciesNotObject)?;
124 if let Some(superseded) = superseded {
125 dependencies.remove(&superseded);
126 }
127 dependencies.insert(name.manifest().to_string(), serialized);
128 dependencies.sort_keys();
129 self.manifest.dependencies.remove(&name);
133 self.manifest.dependencies.insert(name, source.clone());
134 Ok(())
135 }
136
137 pub fn remove_dependency(&mut self, name: &str) -> Result<bool, ManifestDocumentError> {
142 let name = Self::parse_dependency_name(name)?;
143 let Some(existing) = self.matching_dependency_key(&name).map(str::to_string) else {
144 return Ok(false);
145 };
146 let root = self
147 .value
148 .as_object_mut()
149 .ok_or(ManifestDocumentError::RootNotObject)?;
150 let Some(dependencies) = root.get_mut("dependencies") else {
151 return Ok(false);
152 };
153 let removed = dependencies
154 .as_object_mut()
155 .ok_or(ManifestDocumentError::DependenciesNotObject)?
156 .remove(&existing)
157 .is_some();
158 if removed {
159 self.manifest.dependencies.remove(&name);
160 }
161 Ok(removed)
162 }
163
164 pub fn to_bytes(&self) -> Result<Vec<u8>, ManifestDocumentError> {
167 let mut bytes = serde_json::to_vec_pretty(&self.value)?;
168 bytes.push(b'\n');
169 Ok(bytes)
170 }
171
172 fn parse_dependency_name(name: &str) -> Result<DependencyName, ManifestDocumentError> {
174 name.parse().map_err(|source| {
175 ManifestDocumentError::Manifest(ManifestError::InvalidDependencyName {
176 name: name.to_string(),
177 source,
178 })
179 })
180 }
181
182 fn matching_dependency_key(&self, name: &DependencyName) -> Option<&str> {
184 self.manifest
185 .dependencies
186 .keys()
187 .find(|existing| *existing == name)
188 .map(DependencyName::manifest)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::dependency::DependencySource;
196
197 const MANIFEST: &str = r#"{
198 "name": "example",
199 "license": "MIT",
200 "x-extra": { "keep": true },
201 "dependencies": {
202 "zeta": { "path": "./zeta" },
203 "alpha": { "path": "./alpha", "x-source-extra": 7 }
204 }
205 }"#;
206
207 #[test]
208 fn edits_preserve_extensions_and_sort_dependencies() {
209 let mut document = ManifestDocument::parse(MANIFEST.as_bytes()).unwrap();
210 let source: DependencySource =
211 serde_json::from_str(r#"{"path":"./beta","x-source-extra":"kept"}"#).unwrap();
212
213 document.insert_dependency("beta", &source).unwrap();
214
215 let value: serde_json::Value =
216 serde_json::from_slice(&document.to_bytes().unwrap()).unwrap();
217 assert_eq!(value["x-extra"]["keep"], true);
218 assert_eq!(value["dependencies"]["alpha"]["x-source-extra"], 7);
219 assert_eq!(value["dependencies"]["beta"]["x-source-extra"], "kept");
220 assert_eq!(
221 value["dependencies"]
222 .as_object()
223 .unwrap()
224 .keys()
225 .map(String::as_str)
226 .collect::<Vec<_>>(),
227 ["alpha", "beta", "zeta"]
228 );
229 }
230
231 #[test]
232 fn failed_edit_leaves_document_unchanged() {
233 let mut document = ManifestDocument::parse(MANIFEST.as_bytes()).unwrap();
234 let before = document.to_bytes().unwrap();
235 let source: DependencySource =
236 serde_json::from_str(r#"{"path":"./bad","x-source-extra":null}"#).unwrap();
237
238 let error = document
239 .insert_dependency("not valid", &source)
240 .expect_err("an invalid dependency name must fail strict manifest parsing");
241
242 assert!(error.to_string().contains("manifest"));
243 assert_eq!(document.to_bytes().unwrap(), before);
244 }
245
246 #[test]
247 fn insert_dependency_replaces_equivalent_hyphenated_name() {
248 let manifest = r#"{
249 "name": "example",
250 "license": "MIT",
251 "dependencies": {
252 "spell-book": { "path": "./alpha" }
253 }
254 }"#;
255 let mut document = ManifestDocument::parse(manifest.as_bytes()).unwrap();
256 let replacement: DependencySource =
257 serde_json::from_str(r#"{"path":"./updated"}"#).unwrap();
258
259 document
260 .insert_dependency("spell_book", &replacement)
261 .unwrap();
262
263 let value: serde_json::Value =
264 serde_json::from_slice(&document.to_bytes().unwrap()).unwrap();
265 assert_eq!(value["dependencies"]["spell_book"]["path"], "./updated");
266 assert!(value["dependencies"]["spell-book"].is_null());
267 }
268
269 #[test]
270 fn insert_dependency_respells_the_key_in_the_manifest_view() {
271 let manifest = r#"{
272 "name": "example",
273 "license": "MIT",
274 "dependencies": {
275 "spell-book": { "path": "./alpha" }
276 }
277 }"#;
278 let mut document = ManifestDocument::parse(manifest.as_bytes()).unwrap();
279 let replacement: DependencySource =
280 serde_json::from_str(r#"{"path":"./updated"}"#).unwrap();
281
282 document
283 .insert_dependency("spell_book", &replacement)
284 .unwrap();
285
286 let spellings: Vec<_> = document
287 .manifest()
288 .dependencies
289 .keys()
290 .map(|name| name.manifest().to_string())
291 .collect();
292 assert_eq!(
293 spellings,
294 vec!["spell_book".to_string()],
295 "the manifest view must carry the spelling written to the document"
296 );
297 }
298
299 #[test]
300 fn remove_dependency_matches_equivalent_hyphenated_name() {
301 let manifest = r#"{
302 "name": "example",
303 "license": "MIT",
304 "dependencies": {
305 "spell-book": { "path": "./alpha" }
306 }
307 }"#;
308 let mut document = ManifestDocument::parse(manifest.as_bytes()).unwrap();
309
310 assert!(document.remove_dependency("spell_book").unwrap());
311
312 let value: serde_json::Value =
313 serde_json::from_slice(&document.to_bytes().unwrap()).unwrap();
314 assert!(value["dependencies"]["spell-book"].is_null());
315 }
316
317 #[test]
318 fn remove_reports_presence_and_preserves_other_fields() {
319 let mut document = ManifestDocument::parse(MANIFEST.as_bytes()).unwrap();
320 assert!(document.remove_dependency("alpha").unwrap());
321 assert!(!document.remove_dependency("missing").unwrap());
322
323 let value: serde_json::Value =
324 serde_json::from_slice(&document.to_bytes().unwrap()).unwrap();
325 assert_eq!(value["x-extra"]["keep"], true);
326 assert_eq!(value["dependencies"]["zeta"]["path"], "./zeta");
327 }
328
329 #[test]
330 fn serialization_is_pretty_and_newline_terminated() {
331 let document = ManifestDocument::parse(MANIFEST.as_bytes()).unwrap();
332 let bytes = document.to_bytes().unwrap();
333 assert!(bytes.ends_with(b"\n"));
334 assert!(bytes.windows(2).any(|window| window == b"\n "));
335 }
336}