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    Array(&'static TypedHelp),
48    Option(&'static TypedHelp),
49}
50
51pub trait Help {
52    const HELP: TypedHelp;
53}
54
55macro_rules! impl_help_for_num_types {
56    ($help_type:ident, $($ty:ty),+) => {
57        $(impl Help for $ty {
58            const HELP: TypedHelp = TypedHelp::$help_type;
59        })+
60        $(impl Help for NonZero<$ty> {
61            const HELP: TypedHelp = TypedHelp::$help_type;
62        })+
63    };
64}
65
66macro_rules! impl_help_for_types {
67    ($help_type:ident, $($ty:ty),+) => {
68        $(impl Help for $ty {
69            const HELP: TypedHelp = TypedHelp::$help_type;
70        })+
71    };
72}
73
74macro_rules! impl_help_for_array_types {
75    ($($ty:ty),+) => {
76        $(impl<T> Help for $ty where T: Help {
77            const HELP: TypedHelp = TypedHelp::Array(&T::HELP);
78        })+
79    };
80}
81
82impl_help_for_num_types!(
83    Int, i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
84);
85impl_help_for_types!(Float, f32, f64);
86impl_help_for_types!(Bool, bool);
87impl_help_for_types!(
88    String,
89    &str,
90    Box<str>,
91    String,
92    CStr,
93    CString,
94    &OsStr,
95    OsString,
96    &Path,
97    Box<Path>,
98    PathBuf
99);
100impl_help_for_array_types!(&[T], Box<[T]>, Vec<T>);
101
102impl<T> Help for Option<T>
103where
104    T: Help,
105{
106    const HELP: TypedHelp = TypedHelp::Option(&T::HELP);
107}
108
109#[derive(Debug, Default)]
110struct ExtraHelp<'a> {
111    types: HashSet<&'static str>,
112    helps: Vec<&'a TypedHelp>,
113}
114
115fn add_value_type(s: &mut String, v: &TypedHelp) {
116    let type_s = match v {
117        TypedHelp::Bool => "bool",
118        TypedHelp::Int => "integer",
119        TypedHelp::Float => "float",
120        TypedHelp::String => "string",
121        TypedHelp::Unit => todo!(),
122        TypedHelp::Custom { desc } => desc,
123        TypedHelp::Struct { name, .. } => name,
124        TypedHelp::Enum { name, .. } => name,
125        TypedHelp::Option(o) => return add_value_type(s, o),
126        TypedHelp::Array(t) => {
127            s.push_str("array<");
128            add_value_type(s, t);
129            s.push('>');
130            return;
131        }
132    };
133    s.push_str(type_s);
134}
135
136fn add_extra_help<'a>(extra: &mut ExtraHelp<'a>, v: &'a TypedHelp) {
137    let (TypedHelp::Enum {
138        name,
139        variants: fields,
140    }
141    | TypedHelp::Struct { name, fields }) = v
142    else {
143        return;
144    };
145    if extra.types.insert(name) {
146        extra.helps.push(v);
147        for f in fields.iter() {
148            add_extra_help(extra, &f.ty);
149        }
150    }
151}
152
153fn extra_help(s: &mut String, v: &TypedHelp) {
154    s.push_str("# ");
155    match v {
156        TypedHelp::Struct { name, fields } => {
157            struct_help(s, &mut None, name, fields, 2);
158        }
159        TypedHelp::Enum { name, variants } => {
160            enum_help(s, &mut None, name, variants, 2);
161        }
162        _ => unreachable!(),
163    }
164}
165
166fn next_line(s: &mut String, indent: usize) {
167    s.push('\n');
168    for _ in 0..indent {
169        s.push(' ');
170    }
171}
172
173fn one_key_val<'a>(s: &mut String, extra: &mut Option<&mut ExtraHelp<'a>>, f: &'a FieldHelp) {
174    if f.ident.is_empty() {
175        let fields = match f.ty {
176            TypedHelp::Enum { variants, .. } => variants,
177            _ => unreachable!(),
178        };
179        s.push('(');
180        let mut need_separator = false;
181        for field in fields {
182            if need_separator {
183                s.push('|');
184            } else {
185                need_separator = true;
186            }
187            one_key_val(s, extra, field)
188        }
189        s.push(')');
190    } else {
191        s.push_str(f.ident);
192        s.push_str("=<");
193        add_value_type(s, &f.ty);
194        s.push('>');
195        if let Some(extra) = extra {
196            add_extra_help(extra, &f.ty)
197        }
198    }
199}
200
201fn key_val_pairs<'a>(
202    s: &mut String,
203    extra: &mut Option<&mut ExtraHelp<'a>>,
204    variant: &str,
205    fields: &'a [FieldHelp],
206) {
207    let mut add_comma = false;
208    if !variant.is_empty() {
209        s.push_str(variant);
210        add_comma = true;
211    }
212    for f in fields.iter() {
213        if add_comma {
214            s.push(',');
215        } else {
216            add_comma = true;
217        }
218        one_key_val(s, extra, f);
219    }
220}
221
222fn value_helps(s: &mut String, indent: usize, width: usize, fields: &[FieldHelp]) {
223    for f in fields.iter() {
224        if f.ident.is_empty() {
225            let fields = match f.ty {
226                TypedHelp::Enum { variants, .. } => variants,
227                _ => unreachable!(),
228            };
229            value_helps(s, indent, width, fields)
230        } else if f.doc.is_empty() {
231            continue;
232        } else {
233            next_line(s, indent);
234            let mut first_line = true;
235            for line in f.doc.lines() {
236                if first_line {
237                    s.push_str(&format!("- {:width$}\t{}", f.ident, line, width = width));
238                    first_line = false;
239                } else {
240                    next_line(s, indent + width + 2);
241                    s.push('\t');
242                    s.push_str(line);
243                }
244            }
245        }
246    }
247}
248
249fn fields_ident_len_max(fields: &[FieldHelp]) -> Option<usize> {
250    let ident_len = |field: &FieldHelp| {
251        if !field.ident.is_empty() {
252            return Some(field.ident.len());
253        }
254        match field.ty {
255            TypedHelp::Enum { variants, .. } => fields_ident_len_max(variants),
256            TypedHelp::Struct { fields, .. } => fields_ident_len_max(fields),
257            _ => unreachable!(),
258        }
259    };
260
261    fields.iter().flat_map(ident_len).max()
262}
263
264fn field_helps(s: &mut String, indent: usize, fields: &[FieldHelp]) {
265    let Some(width) = fields_ident_len_max(fields) else {
266        return;
267    };
268    value_helps(s, indent, width, fields)
269}
270
271fn struct_help<'a>(
272    s: &mut String,
273    extra: &mut Option<&mut ExtraHelp<'a>>,
274    desc: &str,
275    fields: &'a [FieldHelp],
276    indent: usize,
277) {
278    s.push_str(desc);
279    next_line(s, indent);
280    s.push_str("* ");
281    key_val_pairs(s, extra, "", fields);
282    field_helps(s, indent + 2, fields);
283}
284
285fn enum_all_unit_help(s: &mut String, variants: &[FieldHelp], indent: usize) -> bool {
286    if variants.iter().any(|f| !matches!(f.ty, TypedHelp::Unit)) {
287        return false;
288    }
289    let Some(width) = variants.iter().map(|f| f.ident.len()).max() else {
290        return false;
291    };
292    for variant in variants.iter() {
293        next_line(s, indent);
294        s.push_str(&format!(
295            "* {:width$}\t{}",
296            variant.ident,
297            variant.doc,
298            width = width
299        ));
300    }
301    true
302}
303
304fn enum_help<'a>(
305    s: &mut String,
306    extra: &mut Option<&mut ExtraHelp<'a>>,
307    doc: &str,
308    variants: &'a [FieldHelp],
309    indent: usize,
310) {
311    s.push_str(doc);
312    if enum_all_unit_help(s, variants, indent) {
313        return;
314    }
315    if variants.is_empty() {
316        next_line(s, indent);
317        s.push_str("No options available");
318    }
319    for variant in variants.iter() {
320        next_line(s, indent);
321        s.push_str("* ");
322        match &variant.ty {
323            TypedHelp::Struct { fields, .. } => {
324                key_val_pairs(s, extra, variant.ident, fields);
325                next_line(s, indent + 2);
326                s.push_str(variant.doc);
327                field_helps(s, indent + 2, fields);
328            }
329            TypedHelp::Unit => {
330                s.push_str(variant.ident);
331                next_line(s, indent + 2);
332                s.push_str(variant.doc);
333            }
334            TypedHelp::String
335            | TypedHelp::Int
336            | TypedHelp::Float
337            | TypedHelp::Bool
338            | TypedHelp::Custom { .. } => {
339                s.push_str(variant.ident);
340                s.push_str(",<");
341                add_value_type(s, &variant.ty);
342                s.push('>');
343                next_line(s, indent + 2);
344                s.push_str(variant.doc);
345            }
346            _ => todo!("{:?}", variant.ty),
347        };
348    }
349}
350
351pub fn help_text<T: Help>(doc: &str) -> String {
352    let help = T::HELP;
353    let mut s = String::new();
354    let mut extra = ExtraHelp::default();
355    match &help {
356        TypedHelp::Struct { fields, .. } => {
357            struct_help(&mut s, &mut Some(&mut extra), doc, fields, 0);
358        }
359        TypedHelp::Enum { variants, .. } => {
360            enum_help(&mut s, &mut Some(&mut extra), doc, variants, 0)
361        }
362        _ => unreachable!("{:?}", help),
363    }
364    for h in extra.helps {
365        next_line(&mut s, 0);
366        extra_help(&mut s, h);
367    }
368    s
369}