1use 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}