1use std::{convert::Infallible, fmt::Display, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4use serde_with::{DeserializeFromStr, SerializeDisplay};
5use strum::{Display, EnumString};
6
7use crate::{Error, Name};
8
9#[derive(Clone, Copy, Debug, Display, EnumString, Eq, PartialEq, Serialize)]
27pub enum PackageType {
28 #[strum(to_string = "debug")]
30 Debug,
31 #[strum(to_string = "pkg")]
33 Package,
34 #[strum(to_string = "src")]
36 Source,
37 #[strum(to_string = "split")]
39 Split,
40}
41
42#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64pub struct PackageDescription(String);
65
66impl PackageDescription {
67 pub fn new(description: &str) -> Self {
69 Self::from(description)
70 }
71}
72
73impl Default for PackageDescription {
74 fn default() -> Self {
79 Self::new("")
80 }
81}
82
83impl FromStr for PackageDescription {
84 type Err = Infallible;
85
86 fn from_str(s: &str) -> Result<Self, Self::Err> {
87 Ok(Self::from(s))
88 }
89}
90
91impl AsRef<str> for PackageDescription {
92 fn as_ref(&self) -> &str {
94 &self.0
95 }
96}
97
98impl From<&str> for PackageDescription {
99 fn from(value: &str) -> Self {
105 let mut description = value.trim().replace(['\n', '\r', '\t'], " ");
107
108 let mut previous = ' ';
110 description.retain(|ch| {
111 if ch == ' ' && previous == ' ' {
112 return false;
113 };
114 previous = ch;
115 true
116 });
117
118 Self(description)
119 }
120}
121
122impl Display for PackageDescription {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 write!(f, "{}", self.0)
125 }
126}
127
128pub type PackageBaseName = Name;
149
150#[derive(Clone, Debug, DeserializeFromStr, PartialEq, SerializeDisplay)]
154pub struct ExtraDataEntry {
155 key: String,
156 value: String,
157}
158
159impl ExtraDataEntry {
160 pub fn new(key: String, value: String) -> Self {
162 Self { key, value }
163 }
164
165 pub fn key(&self) -> &str {
167 &self.key
168 }
169
170 pub fn value(&self) -> &str {
172 &self.value
173 }
174}
175
176impl FromStr for ExtraDataEntry {
177 type Err = Error;
178
179 fn from_str(s: &str) -> Result<Self, Self::Err> {
203 const DELIMITER: char = '=';
204 let mut parts = s.splitn(2, DELIMITER);
205 let key = parts
206 .next()
207 .map(|v| v.trim())
208 .filter(|v| !v.is_empty())
209 .ok_or(Error::MissingComponent { component: "key" })?;
210 let value = parts
211 .next()
212 .map(|v| v.trim())
213 .filter(|v| !v.is_empty())
214 .ok_or(Error::MissingComponent { component: "value" })?;
215 Ok(Self::new(key.to_string(), value.to_string()))
216 }
217}
218
219impl Display for ExtraDataEntry {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 write!(f, "{}={}", self.key, self.value)
222 }
223}
224
225#[derive(Clone, Debug, PartialEq, Serialize)]
232pub struct ExtraData(Vec<ExtraDataEntry>);
233
234impl ExtraData {
235 pub fn pkg_type(&self) -> PackageType {
237 self.0
238 .iter()
239 .find(|v| v.key() == "pkgtype")
240 .map(|v| PackageType::from_str(v.value()).expect("Invalid package type"))
241 .unwrap_or_else(|| unreachable!("Valid xdata should always contain a pkgtype entry."))
242 }
243
244 pub fn len(&self) -> usize {
246 self.0.len()
247 }
248
249 pub fn is_empty(&self) -> bool {
254 self.0.is_empty()
255 }
256}
257
258impl TryFrom<Vec<ExtraDataEntry>> for ExtraData {
259 type Error = Error;
260
261 fn try_from(value: Vec<ExtraDataEntry>) -> Result<Self, Self::Error> {
270 if let Some(pkg_type) = value.iter().find(|v| v.key() == "pkgtype") {
271 let _ = PackageType::from_str(pkg_type.value())?;
272 Ok(Self(value))
273 } else {
274 Err(Error::MissingComponent {
275 component: "extra_data with a valid \"pkgtype\" entry",
276 })
277 }
278 }
279}
280
281impl TryFrom<ExtraDataEntry> for ExtraData {
282 type Error = Error;
283
284 fn try_from(value: ExtraDataEntry) -> Result<Self, Self::Error> {
292 Self::try_from(vec![value])
293 }
294}
295
296impl From<ExtraData> for Vec<ExtraDataEntry> {
297 fn from(value: ExtraData) -> Self {
299 value.0
300 }
301}
302
303impl IntoIterator for ExtraData {
304 type Item = ExtraDataEntry;
305 type IntoIter = std::vec::IntoIter<ExtraDataEntry>;
306
307 fn into_iter(self) -> Self::IntoIter {
309 self.0.into_iter()
310 }
311}
312
313impl AsRef<[ExtraDataEntry]> for ExtraData {
314 fn as_ref(&self) -> &[ExtraDataEntry] {
316 &self.0
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use std::str::FromStr;
323
324 use rstest::rstest;
325 use testresult::TestResult;
326
327 use super::*;
328
329 #[rstest]
330 #[case("debug", Ok(PackageType::Debug))]
331 #[case("pkg", Ok(PackageType::Package))]
332 #[case("src", Ok(PackageType::Source))]
333 #[case("split", Ok(PackageType::Split))]
334 #[case("foo", Err(strum::ParseError::VariantNotFound))]
335 fn pkgtype_from_string(
336 #[case] from_str: &str,
337 #[case] result: Result<PackageType, strum::ParseError>,
338 ) {
339 assert_eq!(PackageType::from_str(from_str), result);
340 }
341
342 #[rstest]
343 #[case(PackageType::Debug, "debug")]
344 #[case(PackageType::Package, "pkg")]
345 #[case(PackageType::Source, "src")]
346 #[case(PackageType::Split, "split")]
347 fn pkgtype_format_string(#[case] pkgtype: PackageType, #[case] pkgtype_str: &str) {
348 assert_eq!(pkgtype_str, format!("{pkgtype}"));
349 }
350
351 #[rstest]
352 #[case("key=value", "key", "value")]
353 #[case("pkgtype=debug", "pkgtype", "debug")]
354 #[case("test-123@.foo_+=1000", "test-123@.foo_+", "1000")]
355 fn extra_data_entry_from_str(
356 #[case] data: &str,
357 #[case] key: &str,
358 #[case] value: &str,
359 ) -> TestResult {
360 let extra_data = ExtraDataEntry::from_str(data)?;
361 assert_eq!(extra_data.key(), key);
362 assert_eq!(extra_data.value(), value);
363 assert_eq!(extra_data.to_string(), data);
364 Ok(())
365 }
366
367 #[rstest]
368 #[case("key", Err(Error::MissingComponent { component: "value" }))]
369 #[case("key=", Err(Error::MissingComponent { component: "value" }))]
370 #[case("=value", Err(Error::MissingComponent { component: "key" }))]
371 fn extra_data_entry_from_str_error(
372 #[case] extra_data: &str,
373 #[case] result: Result<ExtraDataEntry, Error>,
374 ) {
375 assert_eq!(ExtraDataEntry::from_str(extra_data), result);
376 }
377
378 #[rstest]
379 #[case::empty_list(vec![])]
380 #[case::invalid_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=foo")?])]
381 fn extra_data_invalid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
382 assert!(ExtraData::try_from(xdata).is_err());
383 Ok(())
384 }
385
386 #[rstest]
387 #[case::only_pkgtype(vec![ExtraDataEntry::from_str("pkgtype=pkg")?])]
388 #[case::with_additional_xdata_entry(vec![ExtraDataEntry::from_str("pkgtype=pkg")?, ExtraDataEntry::from_str("foo=bar")?])]
389 fn extra_data_valid(#[case] xdata: Vec<ExtraDataEntry>) -> TestResult {
390 let xdata = ExtraData::try_from(xdata)?;
391 assert_eq!(xdata.pkg_type(), PackageType::Package);
392 Ok(())
393 }
394
395 #[rstest]
396 #[case(" trailing ", "trailing")]
397 #[case("in between words", "in between words")]
398 #[case("\nsome\t whitespace\n chars\n", "some whitespace chars")]
399 #[case(" \neverything\t combined\n yeah \n ", "everything combined yeah")]
400 fn package_description(#[case] input: &str, #[case] result: &str) {
401 assert_eq!(PackageDescription::new(input).to_string(), result);
402 }
403}