1use std::{
2 fmt::{Display, Formatter},
3 str::FromStr,
4};
5
6use thiserror::Error;
7use uv_warnings::warn_user_once;
8
9bitflags::bitflags! {
10 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11 pub struct PreviewFeatures: u32 {
12 const PYTHON_INSTALL_DEFAULT = 1 << 0;
13 const PYTHON_UPGRADE = 1 << 1;
14 const JSON_OUTPUT = 1 << 2;
15 const PYLOCK = 1 << 3;
16 const ADD_BOUNDS = 1 << 4;
17 const PACKAGE_CONFLICTS = 1 << 5;
18 const EXTRA_BUILD_DEPENDENCIES = 1 << 6;
19 const DETECT_MODULE_CONFLICTS = 1 << 7;
20 const FORMAT = 1 << 8;
21 const NATIVE_AUTH = 1 << 9;
22 const S3_ENDPOINT = 1 << 10;
23 const CACHE_SIZE = 1 << 11;
24 const INIT_PROJECT_FLAG = 1 << 12;
25 const WORKSPACE_METADATA = 1 << 13;
26 const WORKSPACE_DIR = 1 << 14;
27 const WORKSPACE_LIST = 1 << 15;
28 const SBOM_EXPORT = 1 << 16;
29 }
30}
31
32impl PreviewFeatures {
33 fn flag_as_str(self) -> &'static str {
37 match self {
38 Self::PYTHON_INSTALL_DEFAULT => "python-install-default",
39 Self::PYTHON_UPGRADE => "python-upgrade",
40 Self::JSON_OUTPUT => "json-output",
41 Self::PYLOCK => "pylock",
42 Self::ADD_BOUNDS => "add-bounds",
43 Self::PACKAGE_CONFLICTS => "package-conflicts",
44 Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies",
45 Self::DETECT_MODULE_CONFLICTS => "detect-module-conflicts",
46 Self::FORMAT => "format",
47 Self::NATIVE_AUTH => "native-auth",
48 Self::S3_ENDPOINT => "s3-endpoint",
49 Self::CACHE_SIZE => "cache-size",
50 Self::INIT_PROJECT_FLAG => "init-project-flag",
51 Self::WORKSPACE_METADATA => "workspace-metadata",
52 Self::WORKSPACE_DIR => "workspace-dir",
53 Self::WORKSPACE_LIST => "workspace-list",
54 Self::SBOM_EXPORT => "sbom-export",
55 _ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
56 }
57 }
58}
59
60impl Display for PreviewFeatures {
61 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
62 if self.is_empty() {
63 write!(f, "none")
64 } else {
65 let features: Vec<&str> = self.iter().map(Self::flag_as_str).collect();
66 write!(f, "{}", features.join(","))
67 }
68 }
69}
70
71#[derive(Debug, Error, Clone)]
72pub enum PreviewFeaturesParseError {
73 #[error("Empty string in preview features: {0}")]
74 Empty(String),
75}
76
77impl FromStr for PreviewFeatures {
78 type Err = PreviewFeaturesParseError;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 let mut flags = Self::empty();
82
83 for part in s.split(',') {
84 let part = part.trim();
85 if part.is_empty() {
86 return Err(PreviewFeaturesParseError::Empty(
87 "Empty string in preview features".to_string(),
88 ));
89 }
90
91 let flag = match part {
92 "python-install-default" => Self::PYTHON_INSTALL_DEFAULT,
93 "python-upgrade" => Self::PYTHON_UPGRADE,
94 "json-output" => Self::JSON_OUTPUT,
95 "pylock" => Self::PYLOCK,
96 "add-bounds" => Self::ADD_BOUNDS,
97 "package-conflicts" => Self::PACKAGE_CONFLICTS,
98 "extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES,
99 "detect-module-conflicts" => Self::DETECT_MODULE_CONFLICTS,
100 "format" => Self::FORMAT,
101 "native-auth" => Self::NATIVE_AUTH,
102 "s3-endpoint" => Self::S3_ENDPOINT,
103 "cache-size" => Self::CACHE_SIZE,
104 "init-project-flag" => Self::INIT_PROJECT_FLAG,
105 "workspace-metadata" => Self::WORKSPACE_METADATA,
106 "workspace-dir" => Self::WORKSPACE_DIR,
107 "workspace-list" => Self::WORKSPACE_LIST,
108 "sbom-export" => Self::SBOM_EXPORT,
109 _ => {
110 warn_user_once!("Unknown preview feature: `{part}`");
111 continue;
112 }
113 };
114 flags |= flag;
115 }
116
117 Ok(flags)
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub struct Preview {
123 flags: PreviewFeatures,
124}
125
126impl Preview {
127 pub fn new(flags: PreviewFeatures) -> Self {
128 Self { flags }
129 }
130
131 pub fn all() -> Self {
132 Self::new(PreviewFeatures::all())
133 }
134
135 pub fn from_args(
136 preview: bool,
137 no_preview: bool,
138 preview_features: &[PreviewFeatures],
139 ) -> Self {
140 if no_preview {
141 return Self::default();
142 }
143
144 if preview {
145 return Self::all();
146 }
147
148 let mut flags = PreviewFeatures::empty();
149
150 for features in preview_features {
151 flags |= *features;
152 }
153
154 Self { flags }
155 }
156
157 pub fn is_enabled(&self, flag: PreviewFeatures) -> bool {
158 self.flags.contains(flag)
159 }
160}
161
162impl Display for Preview {
163 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
164 if self.flags.is_empty() {
165 write!(f, "disabled")
166 } else if self.flags == PreviewFeatures::all() {
167 write!(f, "enabled")
168 } else {
169 write!(f, "{}", self.flags)
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_preview_features_from_str() {
180 let features = PreviewFeatures::from_str("python-install-default").unwrap();
182 assert_eq!(features, PreviewFeatures::PYTHON_INSTALL_DEFAULT);
183
184 let features = PreviewFeatures::from_str("python-upgrade,json-output").unwrap();
186 assert!(features.contains(PreviewFeatures::PYTHON_UPGRADE));
187 assert!(features.contains(PreviewFeatures::JSON_OUTPUT));
188 assert!(!features.contains(PreviewFeatures::PYLOCK));
189
190 let features = PreviewFeatures::from_str("pylock , add-bounds").unwrap();
192 assert!(features.contains(PreviewFeatures::PYLOCK));
193 assert!(features.contains(PreviewFeatures::ADD_BOUNDS));
194
195 assert!(PreviewFeatures::from_str("").is_err());
197 assert!(PreviewFeatures::from_str("pylock,").is_err());
198 assert!(PreviewFeatures::from_str(",pylock").is_err());
199
200 let features = PreviewFeatures::from_str("unknown-feature,pylock").unwrap();
202 assert!(features.contains(PreviewFeatures::PYLOCK));
203 assert_eq!(features.bits().count_ones(), 1);
204 }
205
206 #[test]
207 fn test_preview_features_display() {
208 let features = PreviewFeatures::empty();
210 assert_eq!(features.to_string(), "none");
211
212 let features = PreviewFeatures::PYTHON_INSTALL_DEFAULT;
214 assert_eq!(features.to_string(), "python-install-default");
215
216 let features = PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::JSON_OUTPUT;
218 assert_eq!(features.to_string(), "python-upgrade,json-output");
219 }
220
221 #[test]
222 fn test_preview_display() {
223 let preview = Preview::default();
225 assert_eq!(preview.to_string(), "disabled");
226
227 let preview = Preview::all();
229 assert_eq!(preview.to_string(), "enabled");
230
231 let preview = Preview::new(PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::PYLOCK);
233 assert_eq!(preview.to_string(), "python-upgrade,pylock");
234 }
235
236 #[test]
237 fn test_preview_from_args() {
238 let preview = Preview::from_args(true, true, &[]);
240 assert_eq!(preview.to_string(), "disabled");
241
242 let preview = Preview::from_args(true, false, &[]);
244 assert_eq!(preview.to_string(), "enabled");
245
246 let features = vec![
248 PreviewFeatures::PYTHON_UPGRADE,
249 PreviewFeatures::JSON_OUTPUT,
250 ];
251 let preview = Preview::from_args(false, false, &features);
252 assert!(preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE));
253 assert!(preview.is_enabled(PreviewFeatures::JSON_OUTPUT));
254 assert!(!preview.is_enabled(PreviewFeatures::PYLOCK));
255 }
256
257 #[test]
258 fn test_as_str_single_flags() {
259 assert_eq!(
260 PreviewFeatures::PYTHON_INSTALL_DEFAULT.flag_as_str(),
261 "python-install-default"
262 );
263 assert_eq!(
264 PreviewFeatures::PYTHON_UPGRADE.flag_as_str(),
265 "python-upgrade"
266 );
267 assert_eq!(PreviewFeatures::JSON_OUTPUT.flag_as_str(), "json-output");
268 assert_eq!(PreviewFeatures::PYLOCK.flag_as_str(), "pylock");
269 assert_eq!(PreviewFeatures::ADD_BOUNDS.flag_as_str(), "add-bounds");
270 assert_eq!(
271 PreviewFeatures::PACKAGE_CONFLICTS.flag_as_str(),
272 "package-conflicts"
273 );
274 assert_eq!(
275 PreviewFeatures::EXTRA_BUILD_DEPENDENCIES.flag_as_str(),
276 "extra-build-dependencies"
277 );
278 assert_eq!(
279 PreviewFeatures::DETECT_MODULE_CONFLICTS.flag_as_str(),
280 "detect-module-conflicts"
281 );
282 assert_eq!(PreviewFeatures::FORMAT.flag_as_str(), "format");
283 assert_eq!(PreviewFeatures::S3_ENDPOINT.flag_as_str(), "s3-endpoint");
284 assert_eq!(PreviewFeatures::SBOM_EXPORT.flag_as_str(), "sbom-export");
285 }
286
287 #[test]
288 #[should_panic(expected = "`flag_as_str` can only be used for exactly one feature flag")]
289 fn test_as_str_multiple_flags_panics() {
290 let features = PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::JSON_OUTPUT;
291 let _ = features.flag_as_str();
292 }
293}