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