1use std::{
2 fmt::{Display, Formatter},
3 ops::BitOr,
4 str::FromStr,
5};
6
7use enumflags2::{BitFlags, bitflags};
8use thiserror::Error;
9use uv_warnings::warn_user_once;
10
11#[bitflags]
12#[repr(u32)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PreviewFeature {
15 PythonInstallDefault = 1 << 0,
16 PythonUpgrade = 1 << 1,
17 JsonOutput = 1 << 2,
18 Pylock = 1 << 3,
19 AddBounds = 1 << 4,
20 PackageConflicts = 1 << 5,
21 ExtraBuildDependencies = 1 << 6,
22 DetectModuleConflicts = 1 << 7,
23 Format = 1 << 8,
24 NativeAuth = 1 << 9,
25 S3Endpoint = 1 << 10,
26 CacheSize = 1 << 11,
27 InitProjectFlag = 1 << 12,
28 WorkspaceMetadata = 1 << 13,
29 WorkspaceDir = 1 << 14,
30 WorkspaceList = 1 << 15,
31 SbomExport = 1 << 16,
32 AuthHelper = 1 << 17,
33 DirectPublish = 1 << 18,
34 TargetWorkspaceDiscovery = 1 << 19,
35 MetadataJson = 1 << 20,
36 GcsEndpoint = 1 << 21,
37 AdjustUlimit = 1 << 22,
38}
39
40impl PreviewFeature {
41 fn as_str(self) -> &'static str {
43 match self {
44 Self::PythonInstallDefault => "python-install-default",
45 Self::PythonUpgrade => "python-upgrade",
46 Self::JsonOutput => "json-output",
47 Self::Pylock => "pylock",
48 Self::AddBounds => "add-bounds",
49 Self::PackageConflicts => "package-conflicts",
50 Self::ExtraBuildDependencies => "extra-build-dependencies",
51 Self::DetectModuleConflicts => "detect-module-conflicts",
52 Self::Format => "format",
53 Self::NativeAuth => "native-auth",
54 Self::S3Endpoint => "s3-endpoint",
55 Self::CacheSize => "cache-size",
56 Self::InitProjectFlag => "init-project-flag",
57 Self::WorkspaceMetadata => "workspace-metadata",
58 Self::WorkspaceDir => "workspace-dir",
59 Self::WorkspaceList => "workspace-list",
60 Self::SbomExport => "sbom-export",
61 Self::AuthHelper => "auth-helper",
62 Self::DirectPublish => "direct-publish",
63 Self::TargetWorkspaceDiscovery => "target-workspace-discovery",
64 Self::MetadataJson => "metadata-json",
65 Self::GcsEndpoint => "gcs-endpoint",
66 Self::AdjustUlimit => "adjust-ulimit",
67 }
68 }
69}
70
71impl Display for PreviewFeature {
72 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73 write!(f, "{}", self.as_str())
74 }
75}
76
77#[derive(Debug, Error, Clone)]
78#[error("Unknown feature flag")]
79pub struct PreviewFeatureParseError;
80
81impl FromStr for PreviewFeature {
82 type Err = PreviewFeatureParseError;
83
84 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 Ok(match s {
86 "python-install-default" => Self::PythonInstallDefault,
87 "python-upgrade" => Self::PythonUpgrade,
88 "json-output" => Self::JsonOutput,
89 "pylock" => Self::Pylock,
90 "add-bounds" => Self::AddBounds,
91 "package-conflicts" => Self::PackageConflicts,
92 "extra-build-dependencies" => Self::ExtraBuildDependencies,
93 "detect-module-conflicts" => Self::DetectModuleConflicts,
94 "format" => Self::Format,
95 "native-auth" => Self::NativeAuth,
96 "s3-endpoint" => Self::S3Endpoint,
97 "gcs-endpoint" => Self::GcsEndpoint,
98 "cache-size" => Self::CacheSize,
99 "init-project-flag" => Self::InitProjectFlag,
100 "workspace-metadata" => Self::WorkspaceMetadata,
101 "workspace-dir" => Self::WorkspaceDir,
102 "workspace-list" => Self::WorkspaceList,
103 "sbom-export" => Self::SbomExport,
104 "auth-helper" => Self::AuthHelper,
105 "direct-publish" => Self::DirectPublish,
106 "target-workspace-discovery" => Self::TargetWorkspaceDiscovery,
107 "metadata-json" => Self::MetadataJson,
108 "adjust-ulimit" => Self::AdjustUlimit,
109 _ => return Err(PreviewFeatureParseError),
110 })
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub struct Preview {
116 flags: BitFlags<PreviewFeature>,
117}
118
119impl Preview {
120 pub fn new(flags: &[PreviewFeature]) -> Self {
121 Self {
122 flags: flags.iter().copied().fold(BitFlags::empty(), BitOr::bitor),
123 }
124 }
125
126 pub fn all() -> Self {
127 Self {
128 flags: BitFlags::all(),
129 }
130 }
131
132 pub fn from_args(preview: bool, no_preview: bool, preview_features: &[PreviewFeature]) -> Self {
133 if no_preview {
134 return Self::default();
135 }
136
137 if preview {
138 return Self::all();
139 }
140
141 Self::new(preview_features)
142 }
143
144 pub fn is_enabled(&self, flag: PreviewFeature) -> bool {
146 self.flags.contains(flag)
147 }
148}
149
150impl Display for Preview {
151 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
152 if self.flags.is_empty() {
153 write!(f, "disabled")
154 } else if self.flags.is_all() {
155 write!(f, "enabled")
156 } else {
157 write!(
158 f,
159 "{}",
160 itertools::join(self.flags.iter().map(PreviewFeature::as_str), ",")
161 )
162 }
163 }
164}
165
166#[derive(Debug, Error, Clone)]
167pub enum PreviewParseError {
168 #[error("Empty string in preview features: {0}")]
169 Empty(String),
170}
171
172impl FromStr for Preview {
173 type Err = PreviewParseError;
174
175 fn from_str(s: &str) -> Result<Self, Self::Err> {
176 let mut flags = BitFlags::empty();
177
178 for part in s.split(',') {
179 let part = part.trim();
180 if part.is_empty() {
181 return Err(PreviewParseError::Empty(
182 "Empty string in preview features".to_string(),
183 ));
184 }
185
186 match PreviewFeature::from_str(part) {
187 Ok(flag) => flags |= flag,
188 Err(_) => {
189 warn_user_once!("Unknown preview feature: `{part}`");
190 }
191 }
192 }
193
194 Ok(Self { flags })
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn test_preview_feature_from_str() {
204 let features = PreviewFeature::from_str("python-install-default").unwrap();
205 assert_eq!(features, PreviewFeature::PythonInstallDefault);
206 }
207
208 #[test]
209 fn test_preview_from_str() {
210 let preview = Preview::from_str("python-install-default").unwrap();
212 assert_eq!(preview.flags, PreviewFeature::PythonInstallDefault);
213
214 let preview = Preview::from_str("python-upgrade,json-output").unwrap();
216 assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
217 assert!(preview.is_enabled(PreviewFeature::JsonOutput));
218 assert_eq!(preview.flags.bits().count_ones(), 2);
219
220 let preview = Preview::from_str("pylock , add-bounds").unwrap();
222 assert!(preview.is_enabled(PreviewFeature::Pylock));
223 assert!(preview.is_enabled(PreviewFeature::AddBounds));
224
225 assert!(Preview::from_str("").is_err());
227 assert!(Preview::from_str("pylock,").is_err());
228 assert!(Preview::from_str(",pylock").is_err());
229
230 let preview = Preview::from_str("unknown-feature,pylock").unwrap();
232 assert!(preview.is_enabled(PreviewFeature::Pylock));
233 assert_eq!(preview.flags.bits().count_ones(), 1);
234 }
235
236 #[test]
237 fn test_preview_display() {
238 let preview = Preview::default();
240 assert_eq!(preview.to_string(), "disabled");
241 let preview = Preview::new(&[]);
242 assert_eq!(preview.to_string(), "disabled");
243
244 let preview = Preview::all();
246 assert_eq!(preview.to_string(), "enabled");
247
248 let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]);
250 assert_eq!(preview.to_string(), "python-install-default");
251
252 let preview = Preview::new(&[PreviewFeature::PythonUpgrade, PreviewFeature::Pylock]);
254 assert_eq!(preview.to_string(), "python-upgrade,pylock");
255 }
256
257 #[test]
258 fn test_preview_from_args() {
259 let preview = Preview::from_args(false, false, &[]);
261 assert_eq!(preview.to_string(), "disabled");
262
263 let preview = Preview::from_args(true, true, &[]);
265 assert_eq!(preview.to_string(), "disabled");
266
267 let preview = Preview::from_args(true, false, &[]);
269 assert_eq!(preview.to_string(), "enabled");
270
271 let features = vec![PreviewFeature::PythonUpgrade, PreviewFeature::JsonOutput];
273 let preview = Preview::from_args(false, false, &features);
274 assert!(preview.is_enabled(PreviewFeature::PythonUpgrade));
275 assert!(preview.is_enabled(PreviewFeature::JsonOutput));
276 assert!(!preview.is_enabled(PreviewFeature::Pylock));
277 }
278
279 #[test]
280 fn test_preview_feature_as_str() {
281 assert_eq!(
282 PreviewFeature::PythonInstallDefault.as_str(),
283 "python-install-default"
284 );
285 assert_eq!(PreviewFeature::PythonUpgrade.as_str(), "python-upgrade");
286 assert_eq!(PreviewFeature::JsonOutput.as_str(), "json-output");
287 assert_eq!(PreviewFeature::Pylock.as_str(), "pylock");
288 assert_eq!(PreviewFeature::AddBounds.as_str(), "add-bounds");
289 assert_eq!(
290 PreviewFeature::PackageConflicts.as_str(),
291 "package-conflicts"
292 );
293 assert_eq!(
294 PreviewFeature::ExtraBuildDependencies.as_str(),
295 "extra-build-dependencies"
296 );
297 assert_eq!(
298 PreviewFeature::DetectModuleConflicts.as_str(),
299 "detect-module-conflicts"
300 );
301 assert_eq!(PreviewFeature::Format.as_str(), "format");
302 assert_eq!(PreviewFeature::NativeAuth.as_str(), "native-auth");
303 assert_eq!(PreviewFeature::S3Endpoint.as_str(), "s3-endpoint");
304 assert_eq!(PreviewFeature::CacheSize.as_str(), "cache-size");
305 assert_eq!(
306 PreviewFeature::InitProjectFlag.as_str(),
307 "init-project-flag"
308 );
309 assert_eq!(
310 PreviewFeature::WorkspaceMetadata.as_str(),
311 "workspace-metadata"
312 );
313 assert_eq!(PreviewFeature::WorkspaceDir.as_str(), "workspace-dir");
314 assert_eq!(PreviewFeature::WorkspaceList.as_str(), "workspace-list");
315 assert_eq!(PreviewFeature::SbomExport.as_str(), "sbom-export");
316 assert_eq!(PreviewFeature::AuthHelper.as_str(), "auth-helper");
317 assert_eq!(PreviewFeature::DirectPublish.as_str(), "direct-publish");
318 assert_eq!(
319 PreviewFeature::TargetWorkspaceDiscovery.as_str(),
320 "target-workspace-discovery"
321 );
322 assert_eq!(PreviewFeature::MetadataJson.as_str(), "metadata-json");
323 assert_eq!(PreviewFeature::GcsEndpoint.as_str(), "gcs-endpoint");
324 assert_eq!(PreviewFeature::AdjustUlimit.as_str(), "adjust-ulimit");
325 }
326}