1use crate::*;
18
19use leo_errors::Backtraced;
20
21use serde::{Deserialize, Serialize};
22use std::path::Path;
23
24pub const MANIFEST_FILENAME: &str = "program.json";
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Manifest {
29 pub program: String,
30 pub version: String,
31 pub description: String,
32 pub license: String,
33 #[serde(default = "current_version")]
34 pub leo: String,
35 pub dependencies: Option<Vec<Dependency>>,
36 pub dev_dependencies: Option<Vec<Dependency>>,
37}
38
39impl Manifest {
40 pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), Backtraced> {
42 let mut contents = serde_json::to_string_pretty(&self)
44 .map_err(|err| crate::errors::failed_to_serialize_manifest_file(path.as_ref().display(), err))?;
45
46 contents.push('\n');
48
49 std::fs::write(path, contents).map_err(crate::errors::failed_to_write_manifest)
51 }
52
53 pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<Self, Backtraced> {
55 let contents = std::fs::read_to_string(&path)
57 .map_err(|_| crate::errors::failed_to_load_package(path.as_ref().display()))?;
58 let manifest: Self = serde_json::from_str(&contents)
60 .map_err(|err| crate::errors::failed_to_deserialize_manifest_file(path.as_ref().display(), err))?;
61 manifest.validate_dependencies()?;
62 Ok(manifest)
63 }
64
65 fn validate_dependencies(&self) -> Result<(), Backtraced> {
66 for dependency in self.dependencies.iter().flatten().chain(self.dev_dependencies.iter().flatten()) {
67 dependency.validate_manifest_shape()?;
68 }
69 Ok(())
70 }
71}
72
73fn current_version() -> String {
75 env!("CARGO_PKG_VERSION").to_string()
76}
77
78impl Dependency {
79 fn validate_manifest_shape(&self) -> Result<(), Backtraced> {
80 match self.location {
81 Location::Network => {
82 if self.path.is_some() {
83 return Err(crate::errors::invalid_manifest_dependency(
84 &self.name,
85 "`network` dependencies cannot specify `path`",
86 ));
87 }
88 }
89 Location::Local => {
90 if self.path.is_none() {
91 return Err(crate::errors::invalid_manifest_dependency(
92 &self.name,
93 "`local` dependencies must specify `path`",
94 ));
95 }
96 if self.edition.is_some() {
97 return Err(crate::errors::invalid_manifest_dependency(
98 &self.name,
99 "`local` dependencies cannot specify `edition`",
100 ));
101 }
102 }
103 Location::Workspace => {
104 if self.path.is_some() {
105 return Err(crate::errors::invalid_manifest_dependency(
106 &self.name,
107 "`workspace` dependencies cannot specify `path`",
108 ));
109 }
110 if self.edition.is_some() {
111 return Err(crate::errors::invalid_manifest_dependency(
112 &self.name,
113 "`workspace` dependencies cannot specify `edition`",
114 ));
115 }
116 }
117 Location::Test => {
118 if self.path.is_none() {
119 return Err(crate::errors::invalid_manifest_dependency(
120 &self.name,
121 "`test` dependencies must specify `path`",
122 ));
123 }
124 if self.edition.is_some() {
125 return Err(crate::errors::invalid_manifest_dependency(
126 &self.name,
127 "`test` dependencies cannot specify `edition`",
128 ));
129 }
130 }
131 }
132 Ok(())
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use std::{
140 fs,
141 time::{SystemTime, UNIX_EPOCH},
142 };
143
144 fn manifest_json(dependencies: &str, dev_dependencies: &str) -> String {
145 format!(
146 r#"{{
147 "program": "test.aleo",
148 "version": "0.1.0",
149 "description": "",
150 "license": "MIT",
151 "dependencies": {dependencies},
152 "dev_dependencies": {dev_dependencies}
153}}"#
154 )
155 }
156
157 fn read_manifest(contents: &str) -> Result<Manifest, Backtraced> {
158 let unique = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
159 let dir = std::env::temp_dir().join(format!("leo-manifest-test-{unique}"));
160 fs::create_dir(&dir).unwrap();
161 let path = dir.join(MANIFEST_FILENAME);
162 fs::write(&path, contents).unwrap();
163 let result = Manifest::read_from_file(&path);
164 fs::remove_dir_all(dir).unwrap();
165 result
166 }
167
168 #[test]
169 fn manifest_rejects_network_dependency_with_path() {
170 let err =
171 read_manifest(&manifest_json(r#"[{"name":"foo.aleo","location":"network","path":"../foo"}]"#, "null"))
172 .unwrap_err();
173
174 assert!(err.to_string().contains("invalid dependency `foo.aleo`"));
175 assert!(err.to_string().contains("`network` dependencies cannot specify `path`"));
176 }
177
178 #[test]
179 fn manifest_rejects_local_dependency_without_path() {
180 let err = read_manifest(&manifest_json(r#"[{"name":"foo.aleo","location":"local"}]"#, "null")).unwrap_err();
181
182 assert!(err.to_string().contains("invalid dependency `foo.aleo`"));
183 assert!(err.to_string().contains("`local` dependencies must specify `path`"));
184 }
185
186 #[test]
187 fn manifest_rejects_invalid_dev_dependency_shape() {
188 let err =
189 read_manifest(&manifest_json("null", r#"[{"name":"foo.aleo","location":"workspace","path":"../foo"}]"#))
190 .unwrap_err();
191
192 assert!(err.to_string().contains("invalid dependency `foo.aleo`"));
193 assert!(err.to_string().contains("`workspace` dependencies cannot specify `path`"));
194 }
195
196 #[test]
197 fn manifest_accepts_location_specific_dependency_fields() {
198 let manifest = read_manifest(&manifest_json(
199 r#"[
200 {"name":"network_dep.aleo","location":"network","edition":1},
201 {"name":"local_dep.aleo","location":"local","path":"../local_dep"},
202 {"name":"workspace_dep.aleo","location":"workspace"}
203]"#,
204 "null",
205 ))
206 .unwrap();
207
208 assert_eq!(manifest.dependencies.unwrap().len(), 3);
209 }
210}