nextest_runner/build_scope/
package_spec.rs1use crate::errors::PackageSpecError;
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
6use std::{
7 fmt,
8 hash::{Hash, Hasher},
9 str::FromStr,
10};
11
12#[derive(Clone, Debug)]
14pub struct PackageSpec {
15 raw: String,
16}
17
18impl PartialEq for PackageSpec {
20 fn eq(&self, other: &Self) -> bool {
21 self.raw == other.raw
22 }
23}
24
25impl Eq for PackageSpec {}
26
27impl Hash for PackageSpec {
28 fn hash<H: Hasher>(&self, state: &mut H) {
29 self.raw.hash(state);
30 }
31}
32
33impl PackageSpec {
34 pub fn new(spec: &str) -> Result<Self, PackageSpecError> {
36 Ok(Self {
37 raw: spec.to_owned(),
38 })
39 }
40
41 pub fn as_str(&self) -> &str {
43 &self.raw
44 }
45}
46
47impl fmt::Display for PackageSpec {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.write_str(&self.raw)
50 }
51}
52
53impl FromStr for PackageSpec {
54 type Err = PackageSpecError;
55
56 fn from_str(spec: &str) -> Result<Self, Self::Err> {
57 Self::new(spec)
58 }
59}
60
61impl Serialize for PackageSpec {
62 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
63 where
64 S: Serializer,
65 {
66 serializer.serialize_str(&self.raw)
67 }
68}
69
70impl<'de> Deserialize<'de> for PackageSpec {
71 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72 where
73 D: Deserializer<'de>,
74 {
75 deserializer.deserialize_str(PackageSpecVisitor)
76 }
77}
78
79struct PackageSpecVisitor;
80
81impl de::Visitor<'_> for PackageSpecVisitor {
82 type Value = PackageSpec;
83
84 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 f.write_str("a Cargo package spec (a package name, name@version, URL, or glob pattern)")
86 }
87
88 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
89 where
90 E: de::Error,
91 {
92 PackageSpec::new(value).map_err(E::custom)
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn serde_round_trip() {
102 let inputs = [
103 "foo",
104 "foo@1.2",
105 "https://github.com/rust-lang/cargo#0.52.0",
106 "nextest-*",
107 ];
108
109 for input in inputs {
110 let spec = PackageSpec::new(input).expect("spec is valid");
111 assert_eq!(spec.as_str(), input, "as_str matches {input}");
112 assert_eq!(spec.to_string(), input, "Display matches {input}");
113 assert_eq!(
114 input.parse::<PackageSpec>().expect("spec parses"),
115 spec,
116 "FromStr matches new for {input}"
117 );
118 let json = serde_json::to_string(&spec).expect("serialized spec");
119 assert_eq!(
120 json,
121 serde_json::to_string(input).expect("serialized input string"),
122 "spec serializes as a plain string for {input}"
123 );
124 let deserialized: PackageSpec = serde_json::from_str(&json).expect("deserialized spec");
125 assert_eq!(deserialized, spec, "round-trip matches for {input}");
126 }
127 }
128
129 #[test]
130 fn deserialize_non_string() {
131 let error =
132 serde_json::from_str::<PackageSpec>("42").expect_err("non-string fails to deserialize");
133 assert_eq!(
134 error.to_string(),
135 "invalid type: integer `42`, expected a Cargo package spec \
136 (a package name, name@version, URL, or glob pattern) at line 1 column 2",
137 );
138 }
139}