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}
262
263#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
264pub struct Deprecated {
265    pub since: Option<&'static str>,
266    pub message: Option<&'static str>,
267}
268
269impl Display for OptionField {
270    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
271        writeln!(f, "{}", self.doc)?;
272        writeln!(f)?;
273
274        writeln!(f, "Default value: {}", self.default)?;
275
276        if let Some(possible_values) = self
277            .possible_values
278            .as_ref()
279            .filter(|values| !values.is_empty())
280        {
281            writeln!(f, "Possible values:")?;
282            writeln!(f)?;
283            for value in possible_values {
284                writeln!(f, "- {value}")?;
285            }
286        } else {
287            writeln!(f, "Type: {}", self.value_type)?;
288        }
289
290        if let Some(deprecated) = &self.deprecated {
291            write!(f, "Deprecated")?;
292
293            if let Some(since) = deprecated.since {
294                write!(f, " (since {since})")?;
295            }
296
297            if let Some(message) = deprecated.message {
298                write!(f, ": {message}")?;
299            }
300
301            writeln!(f)?;
302        }
303
304        writeln!(f, "Example usage:\n```toml\n{}\n```", self.example)
305    }
306}
307
308/// A possible value for an enum, similar to Clap's `PossibleValue` type (but without a dependency
309/// on Clap).
310#[derive(Debug, Eq, PartialEq, Clone, Serialize)]
311pub struct PossibleValue {
312    pub name: String,
313    pub help: Option<String>,
314}
315
316impl Display for PossibleValue {
317    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
318        write!(f, "`\"{}\"`", self.name)?;
319        if let Some(help) = &self.help {
320            write!(f, ": {help}")?;
321        }
322        Ok(())
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_has_child_option() {
332        struct WithOptions;
333
334        impl OptionsMetadata for WithOptions {
335            fn record(visit: &mut dyn Visit) {
336                visit.record_field(
337                    "ignore-git-ignore",
338                    OptionField {
339                        doc: "Whether Ruff should respect the gitignore file",
340                        default: "false",
341                        value_type: "bool",
342                        example: "",
343                        scope: None,
344                        deprecated: None,
345                        possible_values: None,
346                    },
347                );
348            }
349        }
350
351        assert!(WithOptions::metadata().has("ignore-git-ignore"));
352        assert!(!WithOptions::metadata().has("does-not-exist"));
353    }
354
355    #[test]
356    fn test_has_nested_option() {
357        struct Root;
358
359        impl OptionsMetadata for Root {
360            fn record(visit: &mut dyn Visit) {
361                visit.record_field(
362                    "ignore-git-ignore",
363                    OptionField {
364                        doc: "Whether Ruff should respect the gitignore file",
365                        default: "false",
366                        value_type: "bool",
367                        example: "",
368                        scope: None,
369                        deprecated: None,
370                        possible_values: None,
371                    },
372                );
373
374                visit.record_set("format", Nested::metadata());
375            }
376        }
377
378        struct Nested;
379
380        impl OptionsMetadata for Nested {
381            fn record(visit: &mut dyn Visit) {
382                visit.record_field(
383                    "hard-tabs",
384                    OptionField {
385                        doc: "Use hard tabs for indentation and spaces for alignment.",
386                        default: "false",
387                        value_type: "bool",
388                        example: "",
389                        scope: None,
390                        deprecated: None,
391                        possible_values: None,
392                    },
393                );
394            }
395        }
396
397        assert!(Root::metadata().has("format.hard-tabs"));
398        assert!(!Root::metadata().has("format.spaces"));
399        assert!(!Root::metadata().has("lint.hard-tabs"));
400    }
401
402    #[test]
403    fn test_find_child_option() {
404        struct WithOptions;
405
406        static IGNORE_GIT_IGNORE: OptionField = OptionField {
407            doc: "Whether Ruff should respect the gitignore file",
408            default: "false",
409            value_type: "bool",
410            example: "",
411            scope: None,
412            deprecated: None,
413            possible_values: None,
414        };
415
416        impl OptionsMetadata for WithOptions {
417            fn record(visit: &mut dyn Visit) {
418                visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone());
419            }
420        }
421
422        assert_eq!(
423            WithOptions::metadata().find("ignore-git-ignore"),
424            Some(OptionEntry::Field(IGNORE_GIT_IGNORE.clone()))
425        );
426        assert_eq!(WithOptions::metadata().find("does-not-exist"), None);
427    }
428
429    #[test]
430    fn test_find_nested_option() {
431        static HARD_TABS: OptionField = OptionField {
432            doc: "Use hard tabs for indentation and spaces for alignment.",
433            default: "false",
434            value_type: "bool",
435            example: "",
436            scope: None,
437            deprecated: None,
438            possible_values: None,
439        };
440
441        struct Root;
442
443        impl OptionsMetadata for Root {
444            fn record(visit: &mut dyn Visit) {
445                visit.record_field(
446                    "ignore-git-ignore",
447                    OptionField {
448                        doc: "Whether Ruff should respect the gitignore file",
449                        default: "false",
450                        value_type: "bool",
451                        example: "",
452                        scope: None,
453                        deprecated: None,
454                        possible_values: None,
455                    },
456                );
457
458                visit.record_set("format", Nested::metadata());
459            }
460        }
461
462        struct Nested;
463
464        impl OptionsMetadata for Nested {
465            fn record(visit: &mut dyn Visit) {
466                visit.record_field("hard-tabs", HARD_TABS.clone());
467            }
468        }
469
470        assert_eq!(
471            Root::metadata().find("format.hard-tabs"),
472            Some(OptionEntry::Field(HARD_TABS.clone()))
473        );
474        assert_eq!(
475            Root::metadata().find("format"),
476            Some(OptionEntry::Set(Nested::metadata()))
477        );
478        assert_eq!(Root::metadata().find("format.spaces"), None);
479        assert_eq!(Root::metadata().find("lint.hard-tabs"), None);
480    }
481}