Skip to main content

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 v = match v {
138        TypedHelp::Array(t) => t,
139        TypedHelp::Option(t) => t,
140        _ => v,
141    };
142    let (TypedHelp::Enum {
143        name,
144        variants: fields,
145    }
146    | TypedHelp::Struct { name, fields }) = v
147    else {
148        return;
149    };
150    if extra.types.insert(name) {
151        extra.helps.push(v);
152        for f in fields.iter() {
153            add_extra_help(extra, &f.ty);
154        }
155    }
156}
157
158fn extra_help(s: &mut String, v: &TypedHelp) {
159    s.push_str("# ");
160    match v {
161        TypedHelp::Struct { name, fields } => {
162            struct_help(s, &mut None, name, fields, 2);
163        }
164        TypedHelp::Enum { name, variants } => {
165            enum_help(s, &mut None, name, variants, 2);
166        }
167        _ => unreachable!(),
168    }
169}
170
171fn next_line(s: &mut String, indent: usize) {
172    s.push('\n');
173    for _ in 0..indent {
174        s.push(' ');
175    }
176}
177
178fn one_key_val<'a>(s: &mut String, extra: &mut Option<&mut ExtraHelp<'a>>, f: &'a FieldHelp) {
179    if f.ident.is_empty() {
180        let fields = match f.ty {
181            TypedHelp::Enum { variants, .. } => variants,
182            _ => unreachable!(),
183        };
184        s.push('(');
185        let mut need_separator = false;
186        for field in fields {
187            if need_separator {
188                s.push('|');
189            } else {
190                need_separator = true;
191            }
192            one_key_val(s, extra, field)
193        }
194        s.push(')');
195    } else {
196        s.push_str(f.ident);
197        s.push_str("=<");
198        add_value_type(s, &f.ty);
199        s.push('>');
200        if let Some(extra) = extra {
201            add_extra_help(extra, &f.ty)
202        }
203    }
204}
205
206fn key_val_pairs<'a>(
207    s: &mut String,
208    extra: &mut Option<&mut ExtraHelp<'a>>,
209    variant: &str,
210    fields: &'a [FieldHelp],
211) {
212    let mut add_comma = false;
213    if !variant.is_empty() {
214        s.push_str(variant);
215        add_comma = true;
216    }
217    for f in fields.iter() {
218        if add_comma {
219            s.push(',');
220        } else {
221            add_comma = true;
222        }
223        one_key_val(s, extra, f);
224    }
225}
226
227fn value_helps(s: &mut String, indent: usize, width: usize, fields: &[FieldHelp]) {
228    for f in fields.iter() {
229        if f.ident.is_empty() {
230            let fields = match f.ty {
231                TypedHelp::Enum { variants, .. } => variants,
232                _ => unreachable!(),
233            };
234            value_helps(s, indent, width, fields)
235        } else if f.doc.is_empty() {
236            continue;
237        } else {
238            next_line(s, indent);
239            let mut first_line = true;
240            for line in f.doc.lines() {
241                if first_line {
242                    s.push_str(&format!("- {:width$}\t{}", f.ident, line, width = width));
243                    first_line = false;
244                } else {
245                    next_line(s, indent + width + 2);
246                    s.push('\t');
247                    s.push_str(line);
248                }
249            }
250        }
251    }
252}
253
254fn fields_ident_len_max(fields: &[FieldHelp]) -> Option<usize> {
255    let ident_len = |field: &FieldHelp| {
256        if !field.ident.is_empty() {
257            return Some(field.ident.len());
258        }
259        match field.ty {
260            TypedHelp::Enum { variants, .. } => fields_ident_len_max(variants),
261            TypedHelp::Struct { fields, .. } => fields_ident_len_max(fields),
262            _ => unreachable!(),
263        }
264    };
265
266    fields.iter().flat_map(ident_len).max()
267}
268
269fn field_helps(s: &mut String, indent: usize, fields: &[FieldHelp]) {
270    let Some(width) = fields_ident_len_max(fields) else {
271        return;
272    };
273    value_helps(s, indent, width, fields)
274}
275
276fn struct_help<'a>(
277    s: &mut String,
278    extra: &mut Option<&mut ExtraHelp<'a>>,
279    desc: &str,
280    fields: &'a [FieldHelp],
281    indent: usize,
282) {
283    s.push_str(desc);
284    next_line(s, indent);
285    s.push_str("* ");
286    key_val_pairs(s, extra, "", fields);
287    field_helps(s, indent + 2, fields);
288}
289
290fn enum_all_unit_help(s: &mut String, variants: &[FieldHelp], indent: usize) -> bool {
291    if variants.iter().any(|f| !matches!(f.ty, TypedHelp::Unit)) {
292        return false;
293    }
294    let Some(width) = variants.iter().map(|f| f.ident.len()).max() else {
295        return false;
296    };
297    for variant in variants.iter() {
298        next_line(s, indent);
299        s.push_str(&format!(
300            "* {:width$}\t{}",
301            variant.ident,
302            variant.doc,
303            width = width
304        ));
305    }
306    true
307}
308
309fn enum_help<'a>(
310    s: &mut String,
311    extra: &mut Option<&mut ExtraHelp<'a>>,
312    doc: &str,
313    variants: &'a [FieldHelp],
314    indent: usize,
315) {
316    s.push_str(doc);
317    if enum_all_unit_help(s, variants, indent) {
318        return;
319    }
320    if variants.is_empty() {
321        next_line(s, indent);
322        s.push_str("No options available");
323    }
324    for variant in variants.iter() {
325        next_line(s, indent);
326        s.push_str("* ");
327        match &variant.ty {
328            TypedHelp::Struct { fields, .. } => {
329                key_val_pairs(s, extra, variant.ident, fields);
330                next_line(s, indent + 2);
331                s.push_str(variant.doc);
332                field_helps(s, indent + 2, fields);
333            }
334            TypedHelp::Unit => {
335                s.push_str(variant.ident);
336                next_line(s, indent + 2);
337                s.push_str(variant.doc);
338            }
339            TypedHelp::String
340            | TypedHelp::Int
341            | TypedHelp::Float
342            | TypedHelp::Bool
343            | TypedHelp::Custom { .. } => {
344                s.push_str(variant.ident);
345                s.push_str(",<");
346                add_value_type(s, &variant.ty);
347                s.push('>');
348                next_line(s, indent + 2);
349                s.push_str(variant.doc);
350            }
351            _ => todo!("{:?}", variant.ty),
352        };
353    }
354}
355
356pub fn help_text<T: Help>(doc: &str) -> String {
357    let help = T::HELP;
358    let mut s = String::new();
359    let mut extra = ExtraHelp::default();
360    match &help {
361        TypedHelp::Struct { fields, .. } => {
362            struct_help(&mut s, &mut Some(&mut extra), doc, fields, 0);
363        }
364        TypedHelp::Enum { variants, .. } => {
365            enum_help(&mut s, &mut Some(&mut extra), doc, variants, 0)
366        }
367        _ => unreachable!("{:?}", help),
368    }
369    for h in extra.helps {
370        next_line(&mut s, 0);
371        extra_help(&mut s, h);
372    }
373    s
374}