1use crate::error::{Error, Result};
7use serde_json::{Map, Value};
8use std::path::Path;
9
10#[derive(Debug, Clone)]
11pub struct Lock {
12 pub content_hash: Option<String>,
13 pub packages: Vec<LockPackage>,
14 pub packages_dev: Vec<LockPackage>,
15 pub platform: Vec<(String, String)>,
17 pub platform_dev: Vec<(String, String)>,
18 pub plugin_api_version: Option<String>,
19 pub aliases: Vec<Value>,
20}
21
22#[derive(Debug, Clone)]
23pub struct LockPackage {
24 pub raw: Map<String, Value>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DistKind {
29 Zip,
30 Path,
32 Other,
33 Missing,
34}
35
36impl LockPackage {
37 fn str_field(&self, key: &str) -> Option<&str> {
38 self.raw.get(key).and_then(Value::as_str)
39 }
40
41 pub fn name(&self) -> &str {
42 self.str_field("name").unwrap_or("")
43 }
44
45 pub fn version(&self) -> &str {
46 self.str_field("version").unwrap_or("")
47 }
48
49 pub fn package_type(&self) -> &str {
51 self.str_field("type").unwrap_or("library")
52 }
53
54 pub fn dist_url(&self) -> Option<&str> {
55 self.raw.get("dist")?.get("url")?.as_str()
56 }
57
58 pub fn dist_reference(&self) -> Option<&str> {
59 self.raw.get("dist")?.get("reference")?.as_str()
60 }
61
62 pub fn dist_shasum(&self) -> Option<&str> {
64 self.raw
65 .get("dist")?
66 .get("shasum")?
67 .as_str()
68 .filter(|s| !s.is_empty())
69 }
70
71 pub fn dist_kind(&self) -> DistKind {
72 match self
73 .raw
74 .get("dist")
75 .and_then(|d| d.get("type"))
76 .and_then(Value::as_str)
77 {
78 Some("zip") => DistKind::Zip,
79 Some("path") => DistKind::Path,
80 Some(_) => DistKind::Other,
81 None => DistKind::Missing,
82 }
83 }
84
85 pub fn target_dir(&self) -> Option<&str> {
88 self.str_field("target-dir")
89 .map(|t| t.trim_matches('/'))
90 .filter(|t| !t.is_empty())
91 }
92
93 pub fn install_subpath(&self) -> String {
95 match self.target_dir() {
96 Some(t) => format!("{}/{}", self.name(), t),
97 None => self.name().to_owned(),
98 }
99 }
100
101 pub fn is_metapackage(&self) -> bool {
102 self.package_type() == "metapackage"
103 }
104
105 pub fn bins(&self) -> Vec<&str> {
106 self.raw
107 .get("bin")
108 .and_then(Value::as_array)
109 .map(|a| a.iter().filter_map(Value::as_str).collect())
110 .unwrap_or_default()
111 }
112}
113
114fn parse_packages(v: Option<&Value>) -> Vec<LockPackage> {
115 v.and_then(Value::as_array)
116 .map(|a| {
117 a.iter()
118 .filter_map(|p| p.as_object())
119 .map(|m| LockPackage { raw: m.clone() })
120 .collect()
121 })
122 .unwrap_or_default()
123}
124
125fn parse_platform(v: Option<&Value>) -> Vec<(String, String)> {
128 v.and_then(Value::as_object)
129 .map(|m| {
130 m.iter()
131 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_owned())))
132 .collect()
133 })
134 .unwrap_or_default()
135}
136
137impl Lock {
138 pub fn parse(text: &str) -> Result<Self> {
139 let v: Value = serde_json::from_str(text).map_err(|source| Error::Json {
140 context: "composer.lock".to_owned(),
141 source,
142 })?;
143 Ok(Lock {
144 content_hash: v
145 .get("content-hash")
146 .and_then(Value::as_str)
147 .map(str::to_owned),
148 packages: parse_packages(v.get("packages")),
149 packages_dev: parse_packages(v.get("packages-dev")),
150 platform: parse_platform(v.get("platform")),
151 platform_dev: parse_platform(v.get("platform-dev")),
152 plugin_api_version: v
153 .get("plugin-api-version")
154 .and_then(Value::as_str)
155 .map(str::to_owned),
156 aliases: v
157 .get("aliases")
158 .and_then(Value::as_array)
159 .cloned()
160 .unwrap_or_default(),
161 })
162 }
163
164 pub fn read(path: &Path) -> Result<Self> {
165 let text = std::fs::read_to_string(path).map_err(|source| Error::ReadFile {
166 path: path.to_path_buf(),
167 source,
168 })?;
169 Self::parse(&text)
170 }
171
172 pub fn wanted_packages(&self, with_dev: bool) -> impl Iterator<Item = &LockPackage> {
174 self.packages
175 .iter()
176 .chain(
177 self.packages_dev
178 .iter()
179 .take(if with_dev { usize::MAX } else { 0 }),
180 )
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn parses_minimal_lock() {
190 let lock = Lock::parse(
191 r#"{"content-hash":"abc","packages":[{"name":"a/b","version":"1.0.0",
192 "dist":{"type":"zip","url":"https://x/y.zip","reference":"deadbeef","shasum":""},
193 "type":"library","bin":["bin/tool"]}],
194 "packages-dev":[],"platform":{"php":">=8.1"},"platform-dev":[]}"#,
195 )
196 .expect("parse");
197 assert_eq!(lock.content_hash.as_deref(), Some("abc"));
198 let p = &lock.packages[0];
199 assert_eq!(p.name(), "a/b");
200 assert_eq!(p.dist_kind(), DistKind::Zip);
201 assert_eq!(p.dist_shasum(), None); assert_eq!(p.bins(), vec!["bin/tool"]);
203 assert_eq!(lock.platform, vec![("php".to_owned(), ">=8.1".to_owned())]);
204 assert_eq!(lock.wanted_packages(false).count(), 1);
205 }
206
207 #[test]
208 fn empty_platform_as_array() {
209 let lock = Lock::parse(r#"{"packages":[],"platform":[]}"#).expect("parse");
210 assert!(lock.platform.is_empty());
211 }
212}