guppy/platform/summaries.rs
1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{errors::TargetSpecError, platform::PlatformSpec};
5use std::sync::Arc;
6pub use target_spec::summaries::{PlatformSummary, TargetFeaturesSummary};
7
8/// A serializable version of [`PlatformSpec`].
9///
10/// Requires the `summaries` feature to be enabled.
11#[derive(Clone, Debug, Eq, PartialEq, Default)]
12pub enum PlatformSpecSummary {
13 /// The intersection of all platforms.
14 ///
15 /// This is converted to and from [`PlatformSpec::Always`], and is expressed as the string
16 /// `"always"`, or as `spec = "always"`.
17 ///
18 /// # Examples
19 ///
20 /// Deserialize the string `"always"`.
21 ///
22 /// ```
23 /// # use guppy::platform::PlatformSpecSummary;
24 /// let spec: PlatformSpecSummary = serde_json::from_str(r#""always""#).unwrap();
25 /// assert_eq!(spec, PlatformSpecSummary::Always);
26 /// ```
27 ///
28 /// Deserialize `spec = "always"`.
29 ///
30 /// ```
31 /// # use guppy::platform::PlatformSpecSummary;
32 /// let spec: PlatformSpecSummary = toml::from_str(r#"spec = "always""#).unwrap();
33 /// assert_eq!(spec, PlatformSpecSummary::Always);
34 /// ```
35 Always,
36
37 /// An individual platform.
38 ///
39 /// This is converted to and from [`PlatformSpec::Platform`], and is serialized as the platform
40 /// itself (either a triple string, or a map such as
41 /// `{ triple = "x86_64-unknown-linux-gnu", target-features = [] }`).
42 ///
43 /// # Examples
44 ///
45 /// Deserialize a target triple.
46 ///
47 /// ```
48 /// # use guppy::platform::{PlatformSummary, PlatformSpecSummary};
49 /// # use target_spec::summaries::TargetFeaturesSummary;
50 /// # use std::collections::BTreeSet;
51 /// let spec: PlatformSpecSummary = serde_json::from_str(r#""x86_64-unknown-linux-gnu""#).unwrap();
52 /// assert_eq!(
53 /// spec,
54 /// PlatformSpecSummary::Platform(PlatformSummary::new("x86_64-unknown-linux-gnu")),
55 /// );
56 /// ```
57 ///
58 /// Deserialize a target map.
59 ///
60 /// ```
61 /// # use guppy::platform::{PlatformSummary, PlatformSpecSummary};
62 /// # use target_spec::summaries::TargetFeaturesSummary;
63 /// # use std::collections::BTreeSet;
64 /// let spec: PlatformSpecSummary = toml::from_str(r#"
65 /// triple = "x86_64-unknown-linux-gnu"
66 /// target-features = []
67 /// flags = []
68 /// "#).unwrap();
69 /// assert_eq!(
70 /// spec,
71 /// PlatformSpecSummary::Platform(
72 /// PlatformSummary::new("x86_64-unknown-linux-gnu")
73 /// .with_target_features(TargetFeaturesSummary::Features(BTreeSet::new()))
74 /// )
75 /// );
76 /// ```
77 Platform(PlatformSummary),
78
79 /// The union of all platforms.
80 ///
81 /// This is converted to and from [`PlatformSpec::Any`], and is serialized as the string
82 /// `"any"`.
83 ///
84 /// This is also the default, since in many cases one desires to compute the union of enabled
85 /// dependencies across all platforms.
86 ///
87 /// # Examples
88 ///
89 /// Deserialize the string `"any"`.
90 ///
91 /// ```
92 /// # use guppy::platform::PlatformSpecSummary;
93 /// let spec: PlatformSpecSummary = serde_json::from_str(r#""any""#).unwrap();
94 /// assert_eq!(spec, PlatformSpecSummary::Any);
95 /// ```
96 ///
97 /// Deserialize `spec = "any"`.
98 ///
99 /// ```
100 /// # use guppy::platform::PlatformSpecSummary;
101 /// let spec: PlatformSpecSummary = toml::from_str(r#"spec = "any""#).unwrap();
102 /// assert_eq!(spec, PlatformSpecSummary::Any);
103 /// ```
104 #[default]
105 Any,
106}
107
108impl PlatformSpecSummary {
109 /// Creates a new `PlatformSpecSummary` from a [`PlatformSpec`].
110 pub fn new(platform_spec: &PlatformSpec) -> Self {
111 match platform_spec {
112 PlatformSpec::Always => PlatformSpecSummary::Always,
113 PlatformSpec::Platform(platform) => {
114 PlatformSpecSummary::Platform(platform.to_summary())
115 }
116 PlatformSpec::Any => PlatformSpecSummary::Any,
117 }
118 }
119
120 /// Converts `self` to a `PlatformSpec`.
121 ///
122 /// Returns an `Error` if the platform was unknown.
123 pub fn to_platform_spec(&self) -> Result<PlatformSpec, TargetSpecError> {
124 match self {
125 PlatformSpecSummary::Always => Ok(PlatformSpec::Always),
126 PlatformSpecSummary::Platform(platform) => {
127 Ok(PlatformSpec::Platform(Arc::new(platform.to_platform()?)))
128 }
129 PlatformSpecSummary::Any => Ok(PlatformSpec::Any),
130 }
131 }
132
133 /// Returns true if `self` is `PlatformSpecSummary::Any`.
134 pub fn is_any(&self) -> bool {
135 matches!(self, PlatformSpecSummary::Any)
136 }
137}
138
139mod serde_impl {
140 use super::*;
141 use serde::{Deserialize, Deserializer, Serialize, Serializer};
142 use std::collections::BTreeSet;
143 use target_spec::summaries::TargetFeaturesSummary;
144
145 impl Serialize for PlatformSpecSummary {
146 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
147 where
148 S: Serializer,
149 {
150 match self {
151 PlatformSpecSummary::Always => Spec { spec: "always" }.serialize(serializer),
152 PlatformSpecSummary::Any => Spec { spec: "any" }.serialize(serializer),
153 PlatformSpecSummary::Platform(platform) => platform.serialize(serializer),
154 }
155 }
156 }
157
158 // Ideally we'd serialize always or any as just those strings, but that runs into ValueAfterTable
159 // issues with toml. So serialize always/any as "spec = always" etc.
160 #[derive(Serialize)]
161 struct Spec {
162 spec: &'static str,
163 }
164
165 impl<'de> Deserialize<'de> for PlatformSpecSummary {
166 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167 where
168 D: Deserializer<'de>,
169 {
170 match PlatformSpecSummaryDeserialize::deserialize(deserializer)? {
171 PlatformSpecSummaryDeserialize::String(spec)
172 | PlatformSpecSummaryDeserialize::Spec { spec } => {
173 match spec.as_str() {
174 "always" => Ok(PlatformSpecSummary::Always),
175 "any" => Ok(PlatformSpecSummary::Any),
176 _ => {
177 // TODO: expression parsing would go here
178 Ok(PlatformSpecSummary::Platform(PlatformSummary::new(spec)))
179 }
180 }
181 }
182 PlatformSpecSummaryDeserialize::PlatformFull {
183 triple,
184 custom_json,
185 target_features,
186 flags,
187 } => {
188 let mut summary = PlatformSummary::new(triple);
189 summary.custom_json = custom_json;
190 summary.target_features = target_features;
191 summary.flags = flags;
192 Ok(PlatformSpecSummary::Platform(summary))
193 }
194 }
195 }
196 }
197
198 #[derive(Deserialize)]
199 #[serde(untagged)]
200 enum PlatformSpecSummaryDeserialize {
201 String(String),
202 Spec {
203 spec: String,
204 },
205 #[serde(rename_all = "kebab-case")]
206 PlatformFull {
207 // TODO: there doesn't appear to be any way to defer to the PlatformSummary
208 // deserializer, so copy-paste its logic here. Find a better way?
209 triple: String,
210 #[serde(skip_serializing_if = "Option::is_none", default)]
211 custom_json: Option<String>,
212 #[serde(default)]
213 target_features: TargetFeaturesSummary,
214 #[serde(skip_serializing_if = "BTreeSet::is_empty", default)]
215 flags: BTreeSet<String>,
216 },
217 }
218}
219
220#[cfg(all(test, feature = "proptest1"))]
221mod proptests {
222 use super::*;
223 use proptest::prelude::*;
224 use std::collections::HashSet;
225
226 proptest! {
227 #[test]
228 fn summary_roundtrip(platform_spec in any::<PlatformSpec>()) {
229 let summary = PlatformSpecSummary::new(&platform_spec);
230 let serialized = toml::ser::to_string(&summary).expect("serialization succeeded");
231
232 let deserialized: PlatformSpecSummary = toml::from_str(&serialized).expect("deserialization succeeded");
233 assert_eq!(summary, deserialized, "summary and deserialized should match");
234 let platform_spec2 = deserialized.to_platform_spec().expect("conversion to PlatformSpec succeeded");
235
236 match (platform_spec, platform_spec2) {
237 (PlatformSpec::Any, PlatformSpec::Any)
238 | (PlatformSpec::Always, PlatformSpec::Always) => {},
239 (PlatformSpec::Platform(platform), PlatformSpec::Platform(platform2)) => {
240 assert_eq!(platform.triple_str(), platform2.triple_str(), "triples match");
241 assert_eq!(platform.target_features(), platform2.target_features(), "target features match");
242 assert_eq!(platform.flags().collect::<HashSet<_>>(), platform2.flags().collect::<HashSet<_>>(), "flags match");
243 }
244 (other, other2) => panic!("platform specs do not match: original: {:?}, roundtrip: {:?}", other, other2),
245 }
246 }
247 }
248}