blob: 1716944775e51faa1991a37ec57a0ee31b518128 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001use std::fmt::{self, Display};
2use std::path::Path;
3use std::str::FromStr;
4
5use anyhow::{anyhow, bail, Context, Result};
6use regex::Regex;
7use serde::de::Visitor;
8use serde::{Deserialize, Serialize, Serializer};
9
10#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone)]
11pub struct Label {
12 pub repository: Option<String>,
13 pub package: Option<String>,
14 pub target: String,
15}
16
17impl FromStr for Label {
18 type Err = anyhow::Error;
19
20 fn from_str(s: &str) -> Result<Self, Self::Err> {
21 let re = Regex::new(r"^(@[\w\d\-_\.]*)?/{0,2}([\w\d\-_\./]+)?:?([\+\w\d\-_\./]+)$")?;
22 let cap = re
23 .captures(s)
24 .with_context(|| format!("Failed to parse label from string: {}", s))?;
25
26 let repository = cap
27 .get(1)
28 .map(|m| m.as_str().trim_start_matches('@').to_owned());
29 let package = cap.get(2).map(|m| m.as_str().to_owned());
30 let mut target = cap.get(3).map(|m| m.as_str().to_owned());
31
32 if target.is_none() {
33 if let Some(pkg) = &package {
34 target = Some(pkg.clone());
35 } else if let Some(repo) = &repository {
36 target = Some(repo.clone())
37 } else {
38 bail!("The label is missing a label")
39 }
40 }
41
42 // The target should be set at this point
43 let target = target.unwrap();
44
45 Ok(Self {
46 repository,
47 package,
48 target,
49 })
50 }
51}
52
53impl Display for Label {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 let mut label = String::new();
56
57 // Add the repository
58 if let Some(repo) = &self.repository {
59 label = format!("@{}", repo);
60 }
61
62 // Add the package
63 if let Some(pkg) = &self.package {
64 label = format!("{}//{}", label, pkg);
65 }
66
67 write!(f, "{}:{}", &label, &self.target,)
68 }
69}
70
71impl Label {
72 /// Generates a label appropriate for the passed Path by walking the filesystem to identify its
73 /// workspace and package.
74 pub fn from_absolute_path(p: &Path) -> Result<Self, anyhow::Error> {
75 let mut workspace_root = None;
76 let mut package_root = None;
77 for ancestor in p.ancestors().skip(1) {
78 if package_root.is_none()
79 && (ancestor.join("BUILD").exists() || ancestor.join("BUILD.bazel").exists())
80 {
81 package_root = Some(ancestor);
82 }
83 if workspace_root.is_none()
84 && (ancestor.join("WORKSPACE").exists()
85 || ancestor.join("WORKSPACE.bazel").exists())
86 {
87 workspace_root = Some(ancestor);
88 break;
89 }
90 }
91 match (workspace_root, package_root) {
92 (Some(workspace_root), Some(package_root)) => {
93 // These unwraps are safe by construction of the ancestors and prefix calls which set up these paths.
94 let target = p.strip_prefix(package_root).unwrap();
95 let workspace_relative = p.strip_prefix(workspace_root).unwrap();
96 let mut package_path = workspace_relative.to_path_buf();
97 for _ in target.components() {
98 package_path.pop();
99 }
100
101 let package = if package_path.components().count() > 0 {
102 Some(path_to_label_part(&package_path)?)
103 } else {
104 None
105 };
106 let target = path_to_label_part(target)?;
107
108 Ok(Label {
109 repository: None,
110 package,
111 target,
112 })
113 }
114 (Some(_workspace_root), None) => {
115 bail!(
116 "Could not identify package for path {}. Maybe you need to add a BUILD.bazel file.",
117 p.display()
118 );
119 }
120 _ => {
121 bail!("Could not identify workspace for path {}", p.display());
122 }
123 }
124 }
125}
126
127/// Converts a path to a forward-slash-delimited label-appropriate path string.
128fn path_to_label_part(path: &Path) -> Result<String, anyhow::Error> {
129 let components: Result<Vec<_>, _> = path
130 .components()
131 .map(|c| {
132 c.as_os_str().to_str().ok_or_else(|| {
133 anyhow!(
134 "Found non-UTF8 component turning path into label: {}",
135 path.display()
136 )
137 })
138 })
139 .collect();
140 Ok(components?.join("/"))
141}
142
143impl Serialize for Label {
144 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
145 where
146 S: Serializer,
147 {
148 serializer.serialize_str(&self.repr())
149 }
150}
151
152struct LabelVisitor;
153impl<'de> Visitor<'de> for LabelVisitor {
154 type Value = Label;
155
156 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
157 formatter.write_str("Expected string value of `{name} {version}`.")
158 }
159
160 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
161 where
162 E: serde::de::Error,
163 {
164 Label::from_str(v).map_err(E::custom)
165 }
166}
167
168impl<'de> Deserialize<'de> for Label {
169 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
170 where
171 D: serde::Deserializer<'de>,
172 {
173 deserializer.deserialize_str(LabelVisitor)
174 }
175}
176
177impl Label {
178 pub fn repr(&self) -> String {
179 self.to_string()
180 }
181}
182
183#[cfg(test)]
184mod test {
185 use super::*;
186 use spectral::prelude::*;
187 use std::fs::{create_dir_all, File};
188 use tempfile::tempdir;
189
190 #[test]
191 fn full_label() {
192 let label = Label::from_str("@repo//package/sub_package:target").unwrap();
193 assert_eq!(label.repository.unwrap(), "repo");
194 assert_eq!(label.package.unwrap(), "package/sub_package");
195 assert_eq!(label.target, "target");
196 }
197
198 #[test]
199 fn no_repository() {
200 let label = Label::from_str("//package:target").unwrap();
201 assert_eq!(label.repository, None);
202 assert_eq!(label.package.unwrap(), "package");
203 assert_eq!(label.target, "target");
204 }
205
206 #[test]
207 fn no_slashes() {
208 let label = Label::from_str("package:target").unwrap();
209 assert_eq!(label.repository, None);
210 assert_eq!(label.package.unwrap(), "package");
211 assert_eq!(label.target, "target");
212 }
213
214 #[test]
215 fn root_label() {
216 let label = Label::from_str("@repo//:target").unwrap();
217 assert_eq!(label.repository.unwrap(), "repo");
218 assert_eq!(label.package, None);
219 assert_eq!(label.target, "target");
220 }
221
222 #[test]
223 fn root_label_no_repository() {
224 let label = Label::from_str("//:target").unwrap();
225 assert_eq!(label.repository, None);
226 assert_eq!(label.package, None);
227 assert_eq!(label.target, "target");
228 }
229
230 #[test]
231 fn root_label_no_slashes() {
232 let label = Label::from_str(":target").unwrap();
233 assert_eq!(label.repository, None);
234 assert_eq!(label.package, None);
235 assert_eq!(label.target, "target");
236 }
237
238 #[test]
239 fn full_label_with_slash_after_colon() {
240 let label = Label::from_str("@repo//package/sub_package:subdir/target").unwrap();
241 assert_eq!(label.repository.unwrap(), "repo");
242 assert_eq!(label.package.unwrap(), "package/sub_package");
243 assert_eq!(label.target, "subdir/target");
244 }
245
246 #[test]
247 fn invalid_double_colon() {
248 assert!(Label::from_str("::target").is_err());
249 }
250
251 #[test]
252 fn invalid_double_at() {
253 assert!(Label::from_str("@@repo//pkg:target").is_err());
254 }
255
256 #[test]
257 #[ignore = "This currently fails. The Label parsing logic needs to be updated"]
258 fn invalid_no_double_slash() {
259 assert!(Label::from_str("@repo:target").is_err());
260 }
261
262 #[test]
263 fn from_absolute_path_exists() {
264 let dir = tempdir().unwrap();
265 let workspace = dir.path().join("WORKSPACE.bazel");
266 let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
267 let subdir = dir.path().join("parent").join("child").join("grandchild");
268 let actual_file = subdir.join("greatgrandchild");
269 create_dir_all(subdir).unwrap();
270 {
271 File::create(&workspace).unwrap();
272 File::create(&build_file).unwrap();
273 File::create(&actual_file).unwrap();
274 }
275 let label = Label::from_absolute_path(&actual_file).unwrap();
276 assert_eq!(label.repository, None);
277 assert_eq!(label.package.unwrap(), "parent/child");
278 assert_eq!(label.target, "grandchild/greatgrandchild")
279 }
280
281 #[test]
282 fn from_absolute_path_no_workspace() {
283 let dir = tempdir().unwrap();
284 let build_file = dir.path().join("parent").join("child").join("BUILD.bazel");
285 let subdir = dir.path().join("parent").join("child").join("grandchild");
286 let actual_file = subdir.join("greatgrandchild");
287 create_dir_all(subdir).unwrap();
288 {
289 File::create(&build_file).unwrap();
290 File::create(&actual_file).unwrap();
291 }
292 let err = Label::from_absolute_path(&actual_file)
293 .unwrap_err()
294 .to_string();
295 assert_that(&err).contains("Could not identify workspace");
296 assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
297 }
298
299 #[test]
300 fn from_absolute_path_no_build_file() {
301 let dir = tempdir().unwrap();
302 let workspace = dir.path().join("WORKSPACE.bazel");
303 let subdir = dir.path().join("parent").join("child").join("grandchild");
304 let actual_file = subdir.join("greatgrandchild");
305 create_dir_all(subdir).unwrap();
306 {
307 File::create(&workspace).unwrap();
308 File::create(&actual_file).unwrap();
309 }
310 let err = Label::from_absolute_path(&actual_file)
311 .unwrap_err()
312 .to_string();
313 assert_that(&err).contains("Could not identify package");
314 assert_that(&err).contains("Maybe you need to add a BUILD.bazel file");
315 assert_that(&err).contains(format!("{}", actual_file.display()).as_str());
316 }
317}