serde_aco/
help.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashSet;
16use std::ffi::{CStr, CString, OsStr, OsString};
17use std::num::NonZero;
18use std::path::{Path, PathBuf};
19
20pub use serde_aco_derive::Help;
21
22#[derive(Debug)]
23pub struct FieldHelp {
24    pub ident: &'static str,
25    pub doc: &'static str,
26    pub ty: TypedHelp,
27}
28
29#[derive(Debug)]
30pub enum TypedHelp {
31    Struct {
32        name: &'static str,
33        fields: &'static [FieldHelp],
34    },
35    Enum {
36        name: &'static str,
37        variants: &'static [FieldHelp],
38    },
39    String,
40    Int,
41    Float,
42    Bool,
43    Unit,
44    Custom {
45        desc: &'static str,
46    },
47    Option(&'static TypedHelp),
48}
49
50pub trait Help {
51    const HELP: TypedHelp;
52}
53
54macro_rules! impl_help_for_num_types {
55    ($help_type:ident, $($ty:ty),+) => {
56        $(impl Help for $ty {
57            const HELP: TypedHelp = TypedHelp::$help_type;
58        })+
59        $(impl Help for NonZero<$ty> {
60            const HELP: TypedHelp = TypedHelp::$help_type;
61        })+
62    };
63}
64
65macro_rules! impl_help_for_types {
66    ($help_type:ident, $($ty:ty),+) => {
67        $(impl Help for $ty {
68            const HELP: TypedHelp = TypedHelp::$help_type;
69        })+
70    };
71}
72
73impl_help_for_num_types!(Int, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize);
74impl_help_for_types!(Float, f32, f64);
75impl_help_for_types!(Bool, bool);
76impl_help_for_types!(
77    String,
78    &str,
79    Box<str>,
80    String,
81    CStr,
82    CString,
83    &OsStr,
84    OsString,
85    &Path,
86    Box<Path>,
87    PathBuf
88);
89
90impl<T> Help for Option<T>
91where
92    T: Help,
93{
94    const HELP: TypedHelp = TypedHelp::Option(&T::HELP);
95}
96
97#[derive(Debug, Default)]
98struct ExtraHelp<'a> {
99    types: HashSet<&'static str>,
100    helps: Vec<&'a TypedHelp>,
101}
102
103fn value_type(v: &TypedHelp) -> &'static str {
104    match v {
105        TypedHelp::Bool => "bool",
106        TypedHelp::Int => "integer",
107        TypedHelp::Float => "float",
108        TypedHelp::String => "string",
109        TypedHelp::Unit => todo!(),
110        TypedHelp::Custom { desc } => desc,
111        TypedHelp::Struct { name, .. } => name,
112        TypedHelp::Enum { name, .. } => name,
113        TypedHelp::Option(o) => value_type(o),
114    }
115}
116
117fn add_extra_help<'a>(extra: &mut ExtraHelp<'a>, v: &'a TypedHelp) {
118    let (TypedHelp::Enum {
119        name,
120        variants: fields,
121    }
122    | TypedHelp::Struct { name, fields }) = v
123    else {
124        return;
125    };
126    if extra.types.insert(name) {
127        extra.helps.push(v);
128        for f in fields.iter() {
129            add_extra_help(extra, &f.ty);
130        }
131    }
132}
133
134fn extra_help(s: &mut String, v: &TypedHelp) {
135    s.push_str("# ");
136    match v {
137        TypedHelp::Struct { name, fields } => {
138            struct_help(s, &mut None, name, fields, 2);
139        }
140        TypedHelp::Enum { name, variants } => {
141            enum_help(s, &mut None, name, variants, 2);
142        }
143        _ => unreachable!(),
144    }
145}
146
147fn next_line(s: &mut String, indent: usize) {
148    s.push('\n');
149    for _ in 0..indent {
150        s.push(' ');
151    }
152}
153
154fn one_key_val<'a>(s: &mut String, extra: &mut Option<&mut ExtraHelp<'a>>, f: &'a FieldHelp) {
155    if f.ident.is_empty() {
156        let fields = match f.ty {
157            TypedHelp::Enum { variants, .. } => variants,
158            _ => unreachable!(),
159        };
160        s.push('(');
161        let mut need_separator = false;
162        for field in fields {
163            if need_separator {
164                s.push('|');
165            } else {
166                need_separator = true;
167            }
168            one_key_val(s, extra, field)
169        }
170        s.push(')');
171    } else {
172        s.push_str(f.ident);
173        s.push_str("=<");
174        s.push_str(value_type(&f.ty));
175        s.push('>');
176        if let Some(extra) = extra {
177            add_extra_help(extra, &f.ty)
178        }
179    }
180}
181
182fn key_val_pairs<'a>(
183    s: &mut String,
184    extra: &mut Option<&mut ExtraHelp<'a>>,
185    variant: &str,
186    fields: &'a [FieldHelp],
187) {
188    let mut add_comma = false;
189    if !variant.is_empty() {
190        s.push_str(variant);
191        add_comma = true;
192    }
193    for f in fields.iter() {
194        if add_comma {
195            s.push(',');
196        } else {
197            add_comma = true;
198        }
199        one_key_val(s, extra, f);
200    }
201}
202
203fn value_helps(s: &mut String, indent: usize, width: usize, fields: &[FieldHelp]) {
204    for f in fields.iter() {
205        if f.ident.is_empty() {
206            let fields = match f.ty {
207                TypedHelp::Enum { variants, .. } => variants,
208                _ => unreachable!(),
209            };
210            value_helps(s, indent, width, fields)
211        } else if f.doc.is_empty() {
212            continue;
213        } else {
214            next_line(s, indent);
215            let mut first_line = true;
216            for line in f.doc.lines() {
217                if first_line {
218                    s.push_str(&format!("- {:width$}\t{}", f.ident, line, width = width));
219                    first_line = false;
220                } else {
221                    next_line(s, indent + width + 2);
222                    s.push('\t');
223                    s.push_str(line);
224                }
225            }
226        }
227    }
228}
229
230fn fields_ident_len_max(fields: &[FieldHelp]) -> Option<usize> {
231    let ident_len = |field: &FieldHelp| {
232        if !field.ident.is_empty() {
233            return Some(field.ident.len());
234        }
235        match field.ty {
236            TypedHelp::Enum { variants, .. } => fields_ident_len_max(variants),
237            TypedHelp::Struct { fields, .. } => fields_ident_len_max(fields),
238            _ => unreachable!(),
239        }
240    };
241
242    fields.iter().flat_map(ident_len).max()
243}
244
245fn field_helps(s: &mut String, indent: usize, fields: &[FieldHelp]) {
246    let Some(width) = fields_ident_len_max(fields) else {
247        return;
248    };
249    value_helps(s, indent, width, fields)
250}
251
252fn struct_help<'a>(
253    s: &mut String,
254    extra: &mut Option<&mut ExtraHelp<'a>>,
255    desc: &str,
256    fields: &'a [FieldHelp],
257    indent: usize,
258) {
259    s.push_str(desc);
260    next_line(s, indent);
261    s.push_str("* ");
262    key_val_pairs(s, extra, "", fields);
263    field_helps(s, indent + 2, fields);
264}
265
266fn enum_all_unit_help(s: &mut String, variants: &[FieldHelp], indent: usize) -> bool {
267    if variants.iter().any(|f| !matches!(f.ty, TypedHelp::Unit)) {
268        return false;
269    }
270    let Some(width) = variants.iter().map(|f| f.ident.len()).max() else {
271        return false;
272    };
273    for variant in variants.iter() {
274        next_line(s, indent);
275        s.push_str(&format!(
276            "* {:width$}\t{}",
277            variant.ident,
278            variant.doc,
279            width = width
280        ));
281    }
282    true
283}
284
285fn enum_help<'a>(
286    s: &mut String,
287    extra: &mut Option<&mut ExtraHelp<'a>>,
288    doc: &str,
289    variants: &'a [FieldHelp],
290    indent: usize,
291) {
292    s.push_str(doc);
293    if enum_all_unit_help(s, variants, indent) {
294        return;
295    }
296    if variants.is_empty() {
297        next_line(s, indent);
298        s.push_str("No options available");
299    }
300    for variant in variants.iter() {
301        next_line(s, indent);
302        s.push_str("* ");
303        match &variant.ty {
304            TypedHelp::Struct { fields, .. } => {
305                key_val_pairs(s, extra, variant.ident, fields);
306                next_line(s, indent + 2);
307                s.push_str(variant.doc);
308                field_helps(s, indent + 2, fields);
309            }
310            TypedHelp::Unit => {
311                s.push_str(variant.ident);
312                next_line(s, indent + 2);
313                s.push_str(variant.doc);
314            }
315            TypedHelp::String
316            | TypedHelp::Int
317            | TypedHelp::Float
318            | TypedHelp::Bool
319            | TypedHelp::Custom { .. } => {
320                s.push_str(variant.ident);
321                s.push_str(",<");
322                s.push_str(value_type(&variant.ty));
323                s.push('>');
324                next_line(s, indent + 2);
325                s.push_str(variant.doc);
326            }
327            _ => todo!("{:?}", variant.ty),
328        };
329    }
330}
331
332pub fn help_text<T: Help>(doc: &str) -> String {
333    let help = T::HELP;
334    let mut s = String::new();
335    let mut extra = ExtraHelp::default();
336    match &help {
337        TypedHelp::Struct { fields, .. } => {
338            struct_help(&mut s, &mut Some(&mut extra), doc, fields, 0);
339        }
340        TypedHelp::Enum { variants, .. } => {
341            enum_help(&mut s, &mut Some(&mut extra), doc, variants, 0)
342        }
343        _ => unreachable!("{:?}", help),
344    }
345    for h in extra.helps {
346        next_line(&mut s, 0);
347        extra_help(&mut s, h);
348    }
349    s
350}