uv_options_metadata/
lib.rs

1//! Taken directly from Ruff.
2//!
3//! See: <https://github.com/astral-sh/ruff/blob/dc8db1afb08704ad6a788c497068b01edf8b460d/crates/ruff_workspace/sr.rs>
4
5use serde::{Serialize, Serializer};
6use std::collections::BTreeMap;
7
8use std::fmt::{Debug, Display, Formatter};
9
10/// Visits [`OptionsMetadata`].
11///
12/// An instance of [`Visit`] represents the logic for inspecting an object's options metadata.
13pub trait Visit {
14    /// Visits an [`OptionField`] value named `name`.
15    fn record_field(&mut self, name: &str, field: OptionField);
16
17    /// Visits an [`OptionSet`] value named `name`.
18    fn record_set(&mut self, name: &str, group: OptionSet);
19}
20
21/// Returns metadata for its options.
22pub trait OptionsMetadata {
23    /// Visits the options metadata of this object by calling `visit` for each option.
24    fn record(visit: &mut dyn Visit);
25
26    fn documentation() -> Option<&'static str> {
27        None
28    }
29
30    /// Returns the extracted metadata.
31    fn metadata() -> OptionSet
32    where
33        Self: Sized + 'static,
34    {
35        OptionSet::of::<Self>()
36    }
37}
38
39impl<T> OptionsMetadata for Option<T>
40where
41    T: OptionsMetadata,
42{
43    fn record(visit: &mut dyn Visit) {
44        T::record(visit);
45    }
46}
47
48/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`].
49#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
50#[serde(untagged)]
51pub enum OptionEntry {
52    /// A single option.
53    Field(OptionField),
54
55    /// A set of options.
56    Set(OptionSet),
57}
58
59impl Display for OptionEntry {
60    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
61        match self {
62            Self::Set(set) => std::fmt::Display::fmt(set, f),
63            Self::Field(field) => std::fmt::Display::fmt(&field, f),
64        }
65    }
66}
67
68/// A set of options.
69///
70/// It extracts the options by calling the [`OptionsMetadata::record`] of a type implementing
71/// [`OptionsMetadata`].
72#[derive(Copy, Clone)]
73pub struct OptionSet {
74    record: fn(&mut dyn Visit),
75    doc: fn() -> Option<&'static str>,
76}
77
78impl PartialEq for OptionSet {
79    fn eq(&self, other: &Self) -> bool {
80        std::ptr::fn_addr_eq(self.record, other.record) && std::ptr::fn_addr_eq(self.doc, other.doc)
81    }
82}
83
84impl Eq for OptionSet {}
85
86impl OptionSet {
87    pub fn of<T>() -> Self
88    where
89        T: OptionsMetadata + 'static,
90    {
91        Self {
92            record: T::record,
93            doc: T::documentation,
94        }
95    }
96
97    /// Visits the options in this set by calling `visit` for each option.
98    pub fn record(&self, visit: &mut dyn Visit) {
99        let record = self.record;
100        record(visit);
101    }
102
103    pub fn documentation(&self) -> Option<&'static str> {
104        let documentation = self.doc;
105        documentation()
106    }
107
108    /// Returns `true` if this set has an option that resolves to `name`.
109    ///
110    /// The name can be separated by `.` to find a nested option.
111    pub fn has(&self, name: &str) -> bool {
112        self.find(name).is_some()
113    }
114
115    /// Returns `Some` if this set has an option that resolves to `name` and `None` otherwise.
116    ///
117    /// The name can be separated by `.` to find a nested option.
118    pub fn find(&self, name: &str) -> Option<OptionEntry> {
119        struct FindOptionVisitor<'a> {
120            option: Option<OptionEntry>,
121            parts: std::str::Split<'a, char>,
122            needle: &'a str,
123        }
124
125        impl Visit for FindOptionVisitor<'_> {
126            fn record_set(&mut self, name: &str, set: OptionSet) {
127                if self.option.is_none() && name == self.needle {
128                    if let Some(next) = self.parts.next() {
129                        self.needle = next;
130                        set.record(self);
131                    } else {
132                        self.option = Some(OptionEntry::Set(set));
133                    }
134                }
135            }
136
137            fn record_field(&mut self, name: &str, field: OptionField) {
138                if self.option.is_none() && name == self.needle {
139                    if self.parts.next().is_none() {
140                        self.option = Some(OptionEntry::Field(field));
141                    }
142                }
143            }
144        }
145
146        let mut parts = name.split('.');
147
148        if let Some(first) = parts.next() {
149            let mut visitor = FindOptionVisitor {
150                parts,
151                needle: first,
152                option: None,
153            };
154
155            self.record(&mut visitor);
156            visitor.option
157        } else {
158            None
159        }
160    }
161}
162
163/// Visitor that writes out the names of all fields and sets.
164struct DisplayVisitor<'fmt, 'buf> {
165    f: &'fmt mut Formatter<'buf>,
166    result: std::fmt::Result,
167}
168
169impl<'fmt, 'buf> DisplayVisitor<'fmt, 'buf> {
170    fn new(f: &'fmt mut Formatter<'buf>) -> Self {
171        Self { f, result: Ok(()) }
172    }
173
174    fn finish(self) -> std::fmt::Result {
175        self.result
176    }
177}
178
179impl Visit for DisplayVisitor<'_, '_> {
180    fn record_set(&mut self, name: &str, _: OptionSet) {
181        self.result = self.result.and_then(|()| writeln!(self.f, "{name}"));
182    }
183
184    fn record_field(&mut self, name: &str, field: OptionField) {
185        self.result = self.result.and_then(|()| {
186            write!(self.f, "{name}")?;
187
188            if field.deprecated.is_some() {
189                write!(self.f, " (deprecated)")?;
190            }
191
192            writeln!(self.f)
193        });
194    }
195}
196
197impl Display for OptionSet {
198    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
199        let mut visitor = DisplayVisitor::new(f);
200        self.record(&mut visitor);
201        visitor.finish()
202    }
203}
204
205struct SerializeVisitor<'a> {
206    entries: &'a mut BTreeMap<String, OptionField>,
207}
208
209impl Visit for SerializeVisitor<'_> {
210    fn record_set(&mut self, name: &str, set: OptionSet) {
211        // Collect the entries of the set.
212        let mut entries = BTreeMap::new();
213        let mut visitor = SerializeVisitor {
214            entries: &mut entries,
215        };
216        set.record(&mut visitor);
217
218        // Insert the set into the entries.
219        for (key, value) in entries {
220            self.entries.insert(format!("{name}.{key}"), value);
221        }
222    }
223
224    fn record_field(&mut self, name: &str, field: OptionField) {
225        self.entries.insert(name.to_string(), field);
226    }
227}
228
229impl Serialize for OptionSet {
230    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231    where
232        S: Serializer,
233    {
234        let mut entries = BTreeMap::new();
235        let mut visitor = SerializeVisitor {
236            entries: &mut entries,
237        };
238        self.record(&mut visitor);
239        entries.serialize(serializer)
240    }
241}
242
243impl Debug for OptionSet {
244    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
245        Display::fmt(self, f)
246    }
247}
248
249#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
250pub struct OptionField {
251    pub doc: &'static str,
252    /// Ex) `"false"`
253    pub default: &'static str,
254    /// Ex) `"bool"`
255    pub value_type: &'static str,
256    /// Ex) `"per-file-ignores"`
257    pub scope: Option<&'static str>,
258    pub example: &'static str,
259    pub deprecated: Option<Deprecated>,
260    pub possible_values: Option<Vec<PossibleValue>>,
261    /// If true, this option is only available in `uv.toml`, not `pyproject.toml`.
262    pub uv_toml_only: bool,
263}
264
265#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
266pub struct Deprecated {
267    pub since: Option<&'static str>,
268    pub message: Option<&'static str>,
269}
270
271impl Display for OptionField {
272    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
273        writeln!(f, "{}", self.doc)?;
274        writeln!(f)?;
275
276        writeln!(f, "Default value: {}", self.default)?;
277
278        if let Some(possible_values) = self
279            .possible_values
280            .as_ref()
281            .filter(|values| !values.is_empty())
282        {
283            writeln!(f, "Possible values:")?;
284            writeln!(f)?;
285            for value in possible_values {
286                writeln!(f, "- {value}")?;
287            }
288        } else {
289            writeln!(f, "Type: {}", self.value_type)?;
290        }
291
292        if let Some(deprecated) = &self.deprecated {
293            write!(f, "Deprecated")?;
294
295            if let Some(since) = deprecated.since {
296                write!(f, " (since {since})")?;
297            }
298
299            if let Some(message) = deprecated.message {
300                write!(f, ": {message}")?;
301            }
302
303            writeln!(f)?;
304        }
305
306        writeln!(f, "Example usage:\n```toml\n{}\n```", self.example)
307    }
308}
309
310/// A possible value for an enum, similar to Clap's `PossibleValue` type (but without a dependency
311/// on Clap).
312#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
313pub struct PossibleValue {
314    pub name: String,
315    pub help: Option<String>,
316}
317
318impl Display for PossibleValue {
319    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
320        write!(f, "`\"{}\"`", self.name)?;
321        if let Some(help) = &self.help {
322            write!(f, ": {help}")?;
323        }
324        Ok(())
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331
332    #[test]
333    fn test_has_child_option() {
334        struct WithOptions;
335
336        impl OptionsMetadata for WithOptions {
337            fn record(visit: &mut dyn Visit) {
338                visit.record_field(
339                    "ignore-git-ignore",
340                    OptionField {
341                        doc: "Whether Ruff should respect the gitignore file",
342                        default: "false",
343                        value_type: "bool",
344                        example: "",
345                        scope: None,
346                        deprecated: None,
347                        possible_values: None,
348                        uv_toml_only: false,
349                    },
350                );
351            }
352        }
353
354        assert!(WithOptions::metadata().has("ignore-git-ignore"));
355        assert!(!WithOptions::metadata().has("does-not-exist"));
356    }
357
358    #[test]
359    fn test_has_nested_option() {
360        struct Root;
361
362        impl OptionsMetadata for Root {
363            fn record(visit: &mut dyn Visit) {
364                visit.record_field(
365                    "ignore-git-ignore",
366                    OptionField {
367                        doc: "Whether Ruff should respect the gitignore file",
368                        default: "false",
369                        value_type: "bool",
370                        example: "",
371                        scope: None,
372                        deprecated: None,
373                        possible_values: None,
374                        uv_toml_only: false,
375                    },
376                );
377
378                visit.record_set("format", Nested::metadata());
379            }
380        }
381
382        struct Nested;
383
384        impl OptionsMetadata for Nested {
385            fn record(visit: &mut dyn Visit) {
386                visit.record_field(
387                    "hard-tabs",
388                    OptionField {
389                        doc: "Use hard tabs for indentation and spaces for alignment.",
390                        default: "false",
391                        value_type: "bool",
392                        example: "",
393                        scope: None,
394                        deprecated: None,
395                        possible_values: None,
396                        uv_toml_only: false,
397                    },
398                );
399            }
400        }
401
402        assert!(Root::metadata().has("format.hard-tabs"));
403        assert!(!Root::metadata().has("format.spaces"));
404        assert!(!Root::metadata().has("lint.hard-tabs"));
405    }
406
407    #[test]
408    fn test_find_child_option() {
409        struct WithOptions;
410
411        static IGNORE_GIT_IGNORE: OptionField = OptionField {
412            doc: "Whether Ruff should respect the gitignore file",
413            default: "false",
414            value_type: "bool",
415            example: "",
416            scope: None,
417            deprecated: None,
418            possible_values: None,
419            uv_toml_only: false,
420        };
421
422        impl OptionsMetadata for WithOptions {
423            fn record(visit: &mut dyn Visit) {
424                visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone());
425            }
426        }
427
428        assert_eq!(
429            WithOptions::metadata().find("ignore-git-ignore"),
430            Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone()))
431        );
432        assert_eq!(WithOptions::metadata().find("does-not-exist"), None);
433    }
434
435    #[test]
436    fn test_find_nested_option() {
437        static HARD_TABS: OptionField = OptionField {
438            doc: "Use hard tabs for indentation and spaces for alignment.",
439            default: "false",
440            value_type: "bool",
441            example: "",
442            scope: None,
443            deprecated: None,
444            possible_values: None,
445            uv_toml_only: false,
446        };
447
448        struct Root;
449
450        impl OptionsMetadata for Root {
451            fn record(visit: &mut dyn Visit) {
452                visit.record_field(
453                    "ignore-git-ignore",
454                    OptionField {
455                        doc: "Whether Ruff should respect the gitignore file",
456                        default: "false",
457                        value_type: "bool",
458                        example: "",
459                        scope: None,
460                        deprecated: None,
461                        possible_values: None,
462                        uv_toml_only: false,
463                    },
464                );
465
466                visit.record_set("format", Nested::metadata());
467            }
468        }
469
470        struct Nested;
471
472        impl OptionsMetadata for Nested {
473            fn record(visit: &mut dyn Visit) {
474                visit.record_field("hard-tabs", HARD_TABS.clone());
475            }
476        }
477
478        assert_eq!(
479            Root::metadata().find("format.hard-tabs"),
480            Some(OptionEntry::Field(HARD_TABS.clone()))
481        );
482        assert_eq!(
483            Root::metadata().find("format"),
484            Some(OptionEntry::Set(Nested::metadata()))
485        );
486        assert_eq!(Root::metadata().find("format.spaces"), None);
487        assert_eq!(Root::metadata().find("lint.hard-tabs"), None);
488    }
489}