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