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}