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 const AUTH_HELPER = 1 << 17;
30 const DIRECT_PUBLISH = 1 << 18;
31 const TARGET_WORKSPACE_DISCOVERY = 1 << 19;
32 const METADATA_JSON = 1 << 20;
33 const GCS_ENDPOINT = 1 << 21;
34 const ADJUST_ULIMIT = 1 << 22;
35 }
36}
37
38impl PreviewFeatures {
39 fn flag_as_str(self) -> &'static str {
43 match self {
44 Self::PYTHON_INSTALL_DEFAULT => "python-install-default",
45 Self::PYTHON_UPGRADE => "python-upgrade",
46 Self::JSON_OUTPUT => "json-output",
47 Self::PYLOCK => "pylock",
48 Self::ADD_BOUNDS => "add-bounds",
49 Self::PACKAGE_CONFLICTS => "package-conflicts",
50 Self::EXTRA_BUILD_DEPENDENCIES => "extra-build-dependencies",
51 Self::DETECT_MODULE_CONFLICTS => "detect-module-conflicts",
52 Self::FORMAT => "format",
53 Self::NATIVE_AUTH => "native-auth",
54 Self::S3_ENDPOINT => "s3-endpoint",
55 Self::CACHE_SIZE => "cache-size",
56 Self::INIT_PROJECT_FLAG => "init-project-flag",
57 Self::WORKSPACE_METADATA => "workspace-metadata",
58 Self::WORKSPACE_DIR => "workspace-dir",
59 Self::WORKSPACE_LIST => "workspace-list",
60 Self::SBOM_EXPORT => "sbom-export",
61 Self::AUTH_HELPER => "auth-helper",
62 Self::DIRECT_PUBLISH => "direct-publish",
63 Self::TARGET_WORKSPACE_DISCOVERY => "target-workspace-discovery",
64 Self::METADATA_JSON => "metadata-json",
65 Self::GCS_ENDPOINT => "gcs-endpoint",
66 Self::ADJUST_ULIMIT => "adjust-ulimit",
67 _ => panic!("`flag_as_str` can only be used for exactly one feature flag"),
68 }
69 }
70}
71
72impl Display for PreviewFeatures {
73 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
74 if self.is_empty() {
75 write!(f, "none")
76 } else {
77 let features: Vec<&str> = self.iter().map(Self::flag_as_str).collect();
78 write!(f, "{}", features.join(","))
79 }
80 }
81}
82
83#[derive(Debug, Error, Clone)]
84pub enum PreviewFeaturesParseError {
85 #[error("Empty string in preview features: {0}")]
86 Empty(String),
87}
88
89impl FromStr for PreviewFeatures {
90 type Err = PreviewFeaturesParseError;
91
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
93 let mut flags = Self::empty();
94
95 for part in s.split(',') {
96 let part = part.trim();
97 if part.is_empty() {
98 return Err(PreviewFeaturesParseError::Empty(
99 "Empty string in preview features".to_string(),
100 ));
101 }
102
103 let flag = match part {
104 "python-install-default" => Self::PYTHON_INSTALL_DEFAULT,
105 "python-upgrade" => Self::PYTHON_UPGRADE,
106 "json-output" => Self::JSON_OUTPUT,
107 "pylock" => Self::PYLOCK,
108 "add-bounds" => Self::ADD_BOUNDS,
109 "package-conflicts" => Self::PACKAGE_CONFLICTS,
110 "extra-build-dependencies" => Self::EXTRA_BUILD_DEPENDENCIES,
111 "detect-module-conflicts" => Self::DETECT_MODULE_CONFLICTS,
112 "format" => Self::FORMAT,
113 "native-auth" => Self::NATIVE_AUTH,
114 "s3-endpoint" => Self::S3_ENDPOINT,
115 "gcs-endpoint" => Self::GCS_ENDPOINT,
116 "cache-size" => Self::CACHE_SIZE,
117 "init-project-flag" => Self::INIT_PROJECT_FLAG,
118 "workspace-metadata" => Self::WORKSPACE_METADATA,
119 "workspace-dir" => Self::WORKSPACE_DIR,
120 "workspace-list" => Self::WORKSPACE_LIST,
121 "sbom-export" => Self::SBOM_EXPORT,
122 "auth-helper" => Self::AUTH_HELPER,
123 "direct-publish" => Self::DIRECT_PUBLISH,
124 "target-workspace-discovery" => Self::TARGET_WORKSPACE_DISCOVERY,
125 "metadata-json" => Self::METADATA_JSON,
126 "adjust-ulimit" => Self::ADJUST_ULIMIT,
127 _ => {
128 warn_user_once!("Unknown preview feature: `{part}`");
129 continue;
130 }
131 };
132 flags |= flag;
133 }
134
135 Ok(flags)
136 }
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
140pub struct Preview {
141 flags: PreviewFeatures,
142}
143
144impl Preview {
145 pub fn new(flags: PreviewFeatures) -> Self {
146 Self { flags }
147 }
148
149 pub fn all() -> Self {
150 Self::new(PreviewFeatures::all())
151 }
152
153 pub fn from_args(
154 preview: bool,
155 no_preview: bool,
156 preview_features: &[PreviewFeatures],
157 ) -> Self {
158 if no_preview {
159 return Self::default();
160 }
161
162 if preview {
163 return Self::all();
164 }
165
166 let mut flags = PreviewFeatures::empty();
167
168 for features in preview_features {
169 flags |= *features;
170 }
171
172 Self { flags }
173 }
174
175 pub fn is_enabled(&self, flag: PreviewFeatures) -> bool {
176 self.flags.contains(flag)
177 }
178}
179
180impl Display for Preview {
181 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
182 if self.flags.is_empty() {
183 write!(f, "disabled")
184 } else if self.flags == PreviewFeatures::all() {
185 write!(f, "enabled")
186 } else {
187 write!(f, "{}", self.flags)
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_preview_features_from_str() {
198 let features = PreviewFeatures::from_str("python-install-default").unwrap();
200 assert_eq!(features, PreviewFeatures::PYTHON_INSTALL_DEFAULT);
201
202 let features = PreviewFeatures::from_str("python-upgrade,json-output").unwrap();
204 assert!(features.contains(PreviewFeatures::PYTHON_UPGRADE));
205 assert!(features.contains(PreviewFeatures::JSON_OUTPUT));
206 assert!(!features.contains(PreviewFeatures::PYLOCK));
207
208 let features = PreviewFeatures::from_str("pylock , add-bounds").unwrap();
210 assert!(features.contains(PreviewFeatures::PYLOCK));
211 assert!(features.contains(PreviewFeatures::ADD_BOUNDS));
212
213 assert!(PreviewFeatures::from_str("").is_err());
215 assert!(PreviewFeatures::from_str("pylock,").is_err());
216 assert!(PreviewFeatures::from_str(",pylock").is_err());
217
218 let features = PreviewFeatures::from_str("unknown-feature,pylock").unwrap();
220 assert!(features.contains(PreviewFeatures::PYLOCK));
221 assert_eq!(features.bits().count_ones(), 1);
222 }
223
224 #[test]
225 fn test_preview_features_display() {
226 let features = PreviewFeatures::empty();
228 assert_eq!(features.to_string(), "none");
229
230 let features = PreviewFeatures::PYTHON_INSTALL_DEFAULT;
232 assert_eq!(features.to_string(), "python-install-default");
233
234 let features = PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::JSON_OUTPUT;
236 assert_eq!(features.to_string(), "python-upgrade,json-output");
237 }
238
239 #[test]
240 fn test_preview_display() {
241 let preview = Preview::default();
243 assert_eq!(preview.to_string(), "disabled");
244
245 let preview = Preview::all();
247 assert_eq!(preview.to_string(), "enabled");
248
249 let preview = Preview::new(PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::PYLOCK);
251 assert_eq!(preview.to_string(), "python-upgrade,pylock");
252 }
253
254 #[test]
255 fn test_preview_from_args() {
256 let preview = Preview::from_args(true, true, &[]);
258 assert_eq!(preview.to_string(), "disabled");
259
260 let preview = Preview::from_args(true, false, &[]);
262 assert_eq!(preview.to_string(), "enabled");
263
264 let features = vec![
266 PreviewFeatures::PYTHON_UPGRADE,
267 PreviewFeatures::JSON_OUTPUT,
268 ];
269 let preview = Preview::from_args(false, false, &features);
270 assert!(preview.is_enabled(PreviewFeatures::PYTHON_UPGRADE));
271 assert!(preview.is_enabled(PreviewFeatures::JSON_OUTPUT));
272 assert!(!preview.is_enabled(PreviewFeatures::PYLOCK));
273 }
274
275 #[test]
276 fn test_as_str_single_flags() {
277 assert_eq!(
278 PreviewFeatures::PYTHON_INSTALL_DEFAULT.flag_as_str(),
279 "python-install-default"
280 );
281 assert_eq!(
282 PreviewFeatures::PYTHON_UPGRADE.flag_as_str(),
283 "python-upgrade"
284 );
285 assert_eq!(PreviewFeatures::JSON_OUTPUT.flag_as_str(), "json-output");
286 assert_eq!(PreviewFeatures::PYLOCK.flag_as_str(), "pylock");
287 assert_eq!(PreviewFeatures::ADD_BOUNDS.flag_as_str(), "add-bounds");
288 assert_eq!(
289 PreviewFeatures::PACKAGE_CONFLICTS.flag_as_str(),
290 "package-conflicts"
291 );
292 assert_eq!(
293 PreviewFeatures::EXTRA_BUILD_DEPENDENCIES.flag_as_str(),
294 "extra-build-dependencies"
295 );
296 assert_eq!(
297 PreviewFeatures::DETECT_MODULE_CONFLICTS.flag_as_str(),
298 "detect-module-conflicts"
299 );
300 assert_eq!(PreviewFeatures::FORMAT.flag_as_str(), "format");
301 assert_eq!(PreviewFeatures::S3_ENDPOINT.flag_as_str(), "s3-endpoint");
302 assert_eq!(PreviewFeatures::GCS_ENDPOINT.flag_as_str(), "gcs-endpoint");
303 assert_eq!(PreviewFeatures::SBOM_EXPORT.flag_as_str(), "sbom-export");
304 assert_eq!(
305 PreviewFeatures::DIRECT_PUBLISH.flag_as_str(),
306 "direct-publish"
307 );
308 assert_eq!(
309 PreviewFeatures::TARGET_WORKSPACE_DISCOVERY.flag_as_str(),
310 "target-workspace-discovery"
311 );
312 assert_eq!(
313 PreviewFeatures::METADATA_JSON.flag_as_str(),
314 "metadata-json"
315 );
316 assert_eq!(
317 PreviewFeatures::ADJUST_ULIMIT.flag_as_str(),
318 "adjust-ulimit"
319 );
320 }
321
322 #[test]
323 #[should_panic(expected = "`flag_as_str` can only be used for exactly one feature flag")]
324 fn test_as_str_multiple_flags_panics() {
325 let features = PreviewFeatures::PYTHON_UPGRADE | PreviewFeatures::JSON_OUTPUT;
326 let _ = features.flag_as_str();
327 }
328}