1use serde::{Serialize, Serializer};
6use std::collections::BTreeMap;
7
8use std::fmt::{Debug, Display, Formatter};
9
10pub trait Visit {
14 fn record_field(&mut self, name: &str, field: OptionField);
16
17 fn record_set(&mut self, name: &str, group: OptionSet);
19}
20
21pub trait OptionsMetadata {
23 fn record(visit: &mut dyn Visit);
25
26 fn documentation() -> Option<&'static str> {
27 None
28 }
29
30 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#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
50#[serde(untagged)]
51pub enum OptionEntry {
52 Field(OptionField),
54
55 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#[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 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 pub fn has(&self, name: &str) -> bool {
112 self.find(name).is_some()
113 }
114
115 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
163struct 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 let mut entries = BTreeMap::new();
213 let mut visitor = SerializeVisitor {
214 entries: &mut entries,
215 };
216 set.record(&mut visitor);
217
218 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 pub default: &'static str,
254 pub value_type: &'static str,
256 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#[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}