1use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6use std::collections::BTreeMap;
7
8use crate::minify::{MINIFIED_MARKER, expand_versions, minify_versions};
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct PackageDocument {
17 #[serde(skip_serializing_if = "Option::is_none", default)]
21 pub minified: Option<String>,
22 #[serde(default)]
26 pub packages: BTreeMap<String, Vec<Map<String, Value>>>,
27}
28
29impl PackageDocument {
30 pub fn parse(bytes: &[u8]) -> Result<Self, serde_json::Error> {
32 serde_json::from_slice(bytes)
33 }
34
35 pub fn to_vec(&self) -> Result<Vec<u8>, serde_json::Error> {
37 serde_json::to_vec(self)
38 }
39
40 #[must_use]
42 pub fn flat(packages: BTreeMap<String, Vec<Map<String, Value>>>) -> Self {
43 Self {
44 minified: None,
45 packages,
46 }
47 }
48
49 #[must_use]
52 pub fn minified(packages: BTreeMap<String, Vec<Map<String, Value>>>) -> Self {
53 let packages = packages
54 .into_iter()
55 .map(|(name, versions)| (name, minify_versions(&versions)))
56 .collect();
57 Self {
58 minified: Some(MINIFIED_MARKER.to_owned()),
59 packages,
60 }
61 }
62
63 #[must_use]
68 pub fn expand(self) -> BTreeMap<String, Vec<Map<String, Value>>> {
69 let is_minified = self.minified.as_deref() == Some(MINIFIED_MARKER);
70 self.packages
71 .into_iter()
72 .map(|(name, versions)| {
73 let expanded = if is_minified {
74 expand_versions(versions)
75 } else {
76 versions
77 };
78 (name, expanded)
79 })
80 .collect()
81 }
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
90pub struct RootManifest {
91 #[serde(
94 rename = "metadata-url",
95 skip_serializing_if = "Option::is_none",
96 default
97 )]
98 pub metadata_url: Option<String>,
99 #[serde(
101 rename = "available-packages",
102 skip_serializing_if = "Option::is_none",
103 default
104 )]
105 pub available_packages: Option<Vec<String>>,
106 #[serde(
108 rename = "available-package-patterns",
109 skip_serializing_if = "Option::is_none",
110 default
111 )]
112 pub available_package_patterns: Option<Vec<String>>,
113 #[serde(
115 rename = "providers-url",
116 skip_serializing_if = "Option::is_none",
117 default
118 )]
119 pub providers_url: Option<String>,
120 #[serde(flatten)]
123 pub extra: Map<String, Value>,
124}
125
126impl RootManifest {
127 #[must_use]
130 pub fn metadata_template(base_url: &str) -> String {
131 format!("{}/p2/%package%.json", base_url.trim_end_matches('/'))
132 }
133
134 #[must_use]
137 pub fn v2(base_url: &str, available_packages: Vec<String>) -> Self {
138 Self {
139 metadata_url: Some(Self::metadata_template(base_url)),
140 available_packages: Some(available_packages),
141 ..Default::default()
142 }
143 }
144
145 pub fn parse(bytes: &[u8]) -> Result<Self, serde_json::Error> {
147 serde_json::from_slice(bytes)
148 }
149
150 pub fn to_vec(&self) -> Result<Vec<u8>, serde_json::Error> {
152 serde_json::to_vec(self)
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use serde_json::json;
160
161 #[test]
162 fn parses_a_single_fully_expanded_version() {
163 let body = br#"{ "packages": { "monolog/monolog": [
164 {"name":"monolog/monolog","version":"3.0.0","require":{"php":">=8.1"}}
165 ] } }"#;
166 let expanded = PackageDocument::parse(body).unwrap().expand();
167 let v = &expanded["monolog/monolog"];
168 assert_eq!(v.len(), 1);
169 assert_eq!(v[0]["version"], json!("3.0.0"));
170 assert_eq!(v[0]["require"]["php"], json!(">=8.1"));
171 }
172
173 #[test]
174 fn expands_minified_composer_2_0_inheritance() {
175 let body = br#"{ "minified":"composer/2.0", "packages": { "acme/foo": [
176 {"name":"acme/foo","version":"3.0.0","type":"library","dist":{"type":"zip","url":"https://e/3.0.0.zip"},"require":{"php":">=8.1"}},
177 {"version":"2.5.0","dist":{"type":"zip","url":"https://e/2.5.0.zip"}},
178 {"version":"2.0.0","dist":{"type":"zip","url":"https://e/2.0.0.zip"},"require":{"php":">=7.4"},"require-dev":{"phpunit/phpunit":"^9"}}
179 ] } }"#;
180 let expanded = PackageDocument::parse(body).unwrap().expand();
181 let v = &expanded["acme/foo"];
182 assert_eq!(v.len(), 3);
183 assert_eq!(v[0]["version"], json!("3.0.0"));
184 assert_eq!(v[1]["version"], json!("2.5.0"));
186 assert_eq!(v[1]["name"], json!("acme/foo"));
187 assert_eq!(v[1]["type"], json!("library"));
188 assert_eq!(v[1]["require"]["php"], json!(">=8.1"));
189 assert_eq!(v[1]["dist"]["url"], json!("https://e/2.5.0.zip"));
190 assert_eq!(v[2]["require"]["php"], json!(">=7.4"));
192 assert_eq!(v[2]["require-dev"]["phpunit/phpunit"], json!("^9"));
193 assert_eq!(v[2]["name"], json!("acme/foo"));
194 }
195
196 #[test]
197 fn unset_sentinel_resets_inherited_key() {
198 let body = br#"{ "minified":"composer/2.0", "packages": { "acme/bar": [
199 {"name":"acme/bar","version":"2.0.0","require":{"php":">=8.0"}},
200 {"version":"1.0.0","require":"__unset"}
201 ] } }"#;
202 let expanded = PackageDocument::parse(body).unwrap().expand();
203 let v = &expanded["acme/bar"];
204 assert_eq!(v[1]["version"], json!("1.0.0"));
205 assert!(v[1].get("require").is_none());
206 assert_eq!(v[1]["name"], json!("acme/bar"));
207 }
208
209 #[test]
210 fn non_minified_response_is_returned_as_is() {
211 let body = br#"{ "packages": { "acme/baz": [
212 {"name":"acme/baz","version":"2.0.0","require":{"php":">=8.0"}},
213 {"name":"acme/baz","version":"1.0.0"}
214 ] } }"#;
215 let expanded = PackageDocument::parse(body).unwrap().expand();
216 let v = &expanded["acme/baz"];
217 assert_eq!(v[0]["require"]["php"], json!(">=8.0"));
218 assert!(v[1].get("require").is_none());
219 }
220
221 #[test]
222 fn unknown_minified_marker_is_treated_as_non_minified() {
223 let body = br#"{ "minified":"composer/2.1", "packages": { "acme/qux": [
224 {"name":"acme/qux","version":"2.0.0"}
225 ] } }"#;
226 let expanded = PackageDocument::parse(body).unwrap().expand();
227 assert_eq!(expanded["acme/qux"].len(), 1);
228 assert_eq!(expanded["acme/qux"][0]["version"], json!("2.0.0"));
229 }
230
231 #[test]
232 fn empty_packages_map_parses() {
233 let body = br#"{"minified":"composer/2.0","packages":{}}"#;
234 let expanded = PackageDocument::parse(body).unwrap().expand();
235 assert!(expanded.is_empty());
236 }
237
238 #[test]
239 fn malformed_json_errors() {
240 assert!(PackageDocument::parse(br"{not json").is_err());
241 }
242
243 #[test]
244 fn minified_constructor_round_trips_through_expand() {
245 let mut packages = BTreeMap::new();
246 packages.insert(
247 "acme/foo".to_owned(),
248 vec![
249 match json!({"name":"acme/foo","version":"2.0.0","require":{"php":">=8"}}) {
250 Value::Object(m) => m,
251 _ => unreachable!(),
252 },
253 match json!({"name":"acme/foo","version":"1.0.0","require":{"php":">=8"}}) {
254 Value::Object(m) => m,
255 _ => unreachable!(),
256 },
257 ],
258 );
259 let doc = PackageDocument::minified(packages.clone());
260 assert_eq!(doc.minified.as_deref(), Some("composer/2.0"));
261 let bytes = doc.to_vec().unwrap();
263 let expanded = PackageDocument::parse(&bytes).unwrap().expand();
264 assert_eq!(expanded, packages);
265 }
266
267 #[test]
268 fn root_manifest_v2_render_and_round_trip() {
269 let root = RootManifest::v2(
270 "https://r.test/",
271 vec!["acme/widget".to_owned(), "acme/gadget".to_owned()],
272 );
273 assert_eq!(
274 root.metadata_url.as_deref(),
275 Some("https://r.test/p2/%package%.json")
276 );
277 let back = RootManifest::parse(&root.to_vec().unwrap()).unwrap();
278 assert_eq!(back.metadata_url, root.metadata_url);
279 assert_eq!(back.available_packages, root.available_packages);
280 }
281
282 #[test]
283 fn root_manifest_preserves_unknown_fields() {
284 let body = br#"{"metadata-url":"/p2/%package%.json","notify-batch":"https://r/notify"}"#;
285 let root = RootManifest::parse(body).unwrap();
286 assert_eq!(root.metadata_url.as_deref(), Some("/p2/%package%.json"));
287 assert_eq!(root.extra["notify-batch"], json!("https://r/notify"));
288 let back = RootManifest::parse(&root.to_vec().unwrap()).unwrap();
289 assert_eq!(back.extra["notify-batch"], json!("https://r/notify"));
290 }
291}