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