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