Skip to main content

gen_types/
feature.rs

1//! Typed [`Feature`] — named compose-vector of optional capabilities.
2//!
3//! Cargo has `[features]`; npm has nothing native (peerDeps the
4//! closest analog); pip has `extras_require`; Composer has `suggest`;
5//! gem-spec has groups.
6//!
7//! Adapters normalise to this shape; the resolver in `gen-engine`
8//! computes the active feature set per-package from the consumer
9//! tree.
10
11use serde::{Deserialize, Serialize};
12
13/// One named feature. Optional dependencies are expressed via the
14/// implication graph: `feature X implies feature Y` + `feature Y is
15/// "dep:some-optional-dep"`.
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Feature {
18    pub name: String,
19    /// Features this feature transitively enables (Cargo's `["foo",
20    /// "bar/baz"]` shape).
21    pub implies: Vec<FeatureRef>,
22}
23
24/// Reference to a feature by name. Either local (`"derive"` →
25/// this package's `derive` feature) or namespaced (`"serde/derive"`
26/// → another crate's feature).
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(tag = "kind", rename_all = "kebab-case")]
29pub enum FeatureRef {
30    /// Feature of this package.
31    Local { name: String },
32    /// Feature of a different package: `serde/derive`.
33    Namespaced { package: String, feature: String },
34    /// Pseudo-feature that activates an optional dependency
35    /// (Cargo's `dep:foo` syntax).
36    DepActivation { dep_name: String },
37}
38
39impl FeatureRef {
40    /// Parse `serde/derive` / `dep:foo` / `derive` shapes into the
41    /// matching typed variant.
42    #[must_use]
43    pub fn parse(s: &str) -> Self {
44        if let Some(rest) = s.strip_prefix("dep:") {
45            Self::DepActivation {
46                dep_name: rest.to_string(),
47            }
48        } else if let Some((pkg, feat)) = s.split_once('/') {
49            Self::Namespaced {
50                package: pkg.to_string(),
51                feature: feat.to_string(),
52            }
53        } else {
54            Self::Local {
55                name: s.to_string(),
56            }
57        }
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn parse_local_feature() {
67        assert_eq!(
68            FeatureRef::parse("derive"),
69            FeatureRef::Local {
70                name: "derive".into()
71            }
72        );
73    }
74
75    #[test]
76    fn parse_namespaced_feature() {
77        assert_eq!(
78            FeatureRef::parse("serde/derive"),
79            FeatureRef::Namespaced {
80                package: "serde".into(),
81                feature: "derive".into(),
82            }
83        );
84    }
85
86    #[test]
87    fn parse_dep_activation() {
88        assert_eq!(
89            FeatureRef::parse("dep:some-opt"),
90            FeatureRef::DepActivation {
91                dep_name: "some-opt".into()
92            }
93        );
94    }
95
96    #[test]
97    fn round_trip_through_serde() {
98        let f = Feature {
99            name: "default".into(),
100            implies: vec![
101                FeatureRef::Local { name: "std".into() },
102                FeatureRef::Namespaced {
103                    package: "serde".into(),
104                    feature: "derive".into(),
105                },
106            ],
107        };
108        let j = serde_json::to_string(&f).unwrap();
109        let parsed: Feature = serde_json::from_str(&j).unwrap();
110        assert_eq!(f, parsed);
111    }
112}