1use crate::exception::{ExceptionKind, SolverException};
13use crate::throw;
14use crate::types::{Index, Number};
15use std::cell::RefCell;
16use std::collections::BTreeMap;
17use std::rc::Rc;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[allow(non_camel_case_types)]
22pub enum OptionType {
23 OT_Number,
24 OT_Integer,
25 OT_String,
26 OT_Unknown,
27}
28
29#[derive(Debug, Clone)]
31pub struct StringEntry {
32 pub value: String,
33 pub description: String,
34}
35
36#[derive(Debug, Clone)]
37pub enum DefaultValue {
38 None,
39 Number(Number),
40 Integer(Index),
41 String(String),
42}
43
44#[derive(Debug, Clone)]
46pub struct RegisteredOption {
47 pub name: String,
48 pub short_description: String,
49 pub long_description: String,
50 pub category: String,
51 pub counter: Index,
52 pub advanced: bool,
53 pub option_type: OptionType,
54 pub default: DefaultValue,
55 pub has_lower: bool,
56 pub lower: Number,
57 pub lower_strict: bool,
58 pub has_upper: bool,
59 pub upper: Number,
60 pub upper_strict: bool,
61 pub valid_strings: Vec<StringEntry>,
62}
63
64impl RegisteredOption {
65 fn new(
66 name: String,
67 short: String,
68 long: String,
69 category: String,
70 counter: Index,
71 advanced: bool,
72 ) -> Self {
73 Self {
74 name,
75 short_description: short,
76 long_description: long,
77 category,
78 counter,
79 advanced,
80 option_type: OptionType::OT_Unknown,
81 default: DefaultValue::None,
82 has_lower: false,
83 lower: 0.0,
84 lower_strict: false,
85 has_upper: false,
86 upper: 0.0,
87 upper_strict: false,
88 valid_strings: Vec::new(),
89 }
90 }
91
92 pub fn is_valid_number(&self, v: Number) -> bool {
94 if self.has_lower {
95 let ok = if self.lower_strict {
96 v > self.lower
97 } else {
98 v >= self.lower
99 };
100 if !ok {
101 return false;
102 }
103 }
104 if self.has_upper {
105 let ok = if self.upper_strict {
106 v < self.upper
107 } else {
108 v <= self.upper
109 };
110 if !ok {
111 return false;
112 }
113 }
114 true
115 }
116
117 pub fn is_valid_integer(&self, v: Index) -> bool {
118 self.is_valid_number(v as Number)
119 }
120
121 pub fn is_valid_string(&self, value: &str) -> bool {
126 let v = value.to_ascii_lowercase();
127 self.valid_strings
128 .iter()
129 .any(|e| e.value == "*" || e.value.eq_ignore_ascii_case(&v))
130 }
131
132 pub fn canonical_string(&self, value: &str) -> Option<&str> {
135 self.valid_strings
136 .iter()
137 .find(|e| e.value.eq_ignore_ascii_case(value))
138 .map(|e| e.value.as_str())
139 }
140
141 pub fn map_string_to_enum(&self, value: &str) -> Option<Index> {
143 self.valid_strings
144 .iter()
145 .position(|e| e.value.eq_ignore_ascii_case(value))
146 .map(|i| i as Index)
147 }
148}
149
150#[derive(Debug, Default)]
152pub struct RegisteredOptions {
153 options: RefCell<BTreeMap<String, Rc<RegisteredOption>>>,
155 order: RefCell<Vec<String>>,
157 current_category: RefCell<String>,
159 next_counter: RefCell<Index>,
160}
161
162impl RegisteredOptions {
163 pub fn new() -> Rc<Self> {
164 Rc::new(Self::default())
165 }
166
167 pub fn set_registering_category(&self, category: impl Into<String>) {
168 *self.current_category.borrow_mut() = category.into();
169 }
170
171 fn alloc_counter(&self) -> Index {
172 let mut c = self.next_counter.borrow_mut();
173 let v = *c;
174 *c += 1;
175 v
176 }
177
178 fn register(&self, opt: RegisteredOption) -> Result<Rc<RegisteredOption>, SolverException> {
179 let key = opt.name.to_ascii_lowercase();
180 let mut opts = self.options.borrow_mut();
181 if opts.contains_key(&key) {
182 throw!(
183 ExceptionKind::OPTION_ALREADY_REGISTERED,
184 format!("Option {} already registered.", opt.name)
185 );
186 }
187 let rc = Rc::new(opt);
188 opts.insert(key.clone(), rc.clone());
189 self.order.borrow_mut().push(key);
190 Ok(rc)
191 }
192
193 pub fn add_number_option(
194 &self,
195 name: &str,
196 short_description: &str,
197 default_value: Number,
198 long_description: &str,
199 ) -> Result<Rc<RegisteredOption>, SolverException> {
200 let mut o = RegisteredOption::new(
201 name.to_string(),
202 short_description.to_string(),
203 long_description.to_string(),
204 self.current_category.borrow().clone(),
205 self.alloc_counter(),
206 false,
207 );
208 o.option_type = OptionType::OT_Number;
209 o.default = DefaultValue::Number(default_value);
210 self.register(o)
211 }
212
213 pub fn add_lower_bounded_number_option(
214 &self,
215 name: &str,
216 short_description: &str,
217 lower: Number,
218 strict: bool,
219 default_value: Number,
220 long_description: &str,
221 ) -> Result<Rc<RegisteredOption>, SolverException> {
222 let mut o = RegisteredOption::new(
223 name.to_string(),
224 short_description.to_string(),
225 long_description.to_string(),
226 self.current_category.borrow().clone(),
227 self.alloc_counter(),
228 false,
229 );
230 o.option_type = OptionType::OT_Number;
231 o.default = DefaultValue::Number(default_value);
232 o.has_lower = true;
233 o.lower = lower;
234 o.lower_strict = strict;
235 self.register(o)
236 }
237
238 #[allow(clippy::too_many_arguments)]
239 pub fn add_bounded_number_option(
240 &self,
241 name: &str,
242 short_description: &str,
243 lower: Number,
244 lower_strict: bool,
245 upper: Number,
246 upper_strict: bool,
247 default_value: Number,
248 long_description: &str,
249 ) -> Result<Rc<RegisteredOption>, SolverException> {
250 let mut o = RegisteredOption::new(
251 name.to_string(),
252 short_description.to_string(),
253 long_description.to_string(),
254 self.current_category.borrow().clone(),
255 self.alloc_counter(),
256 false,
257 );
258 o.option_type = OptionType::OT_Number;
259 o.default = DefaultValue::Number(default_value);
260 o.has_lower = true;
261 o.lower = lower;
262 o.lower_strict = lower_strict;
263 o.has_upper = true;
264 o.upper = upper;
265 o.upper_strict = upper_strict;
266 self.register(o)
267 }
268
269 pub fn add_integer_option(
270 &self,
271 name: &str,
272 short_description: &str,
273 default_value: Index,
274 long_description: &str,
275 ) -> Result<Rc<RegisteredOption>, SolverException> {
276 let mut o = RegisteredOption::new(
277 name.to_string(),
278 short_description.to_string(),
279 long_description.to_string(),
280 self.current_category.borrow().clone(),
281 self.alloc_counter(),
282 false,
283 );
284 o.option_type = OptionType::OT_Integer;
285 o.default = DefaultValue::Integer(default_value);
286 self.register(o)
287 }
288
289 pub fn add_lower_bounded_integer_option(
290 &self,
291 name: &str,
292 short_description: &str,
293 lower: Index,
294 default_value: Index,
295 long_description: &str,
296 ) -> Result<Rc<RegisteredOption>, SolverException> {
297 let mut o = RegisteredOption::new(
298 name.to_string(),
299 short_description.to_string(),
300 long_description.to_string(),
301 self.current_category.borrow().clone(),
302 self.alloc_counter(),
303 false,
304 );
305 o.option_type = OptionType::OT_Integer;
306 o.default = DefaultValue::Integer(default_value);
307 o.has_lower = true;
308 o.lower = lower as Number;
309 self.register(o)
310 }
311
312 pub fn add_bounded_integer_option(
313 &self,
314 name: &str,
315 short_description: &str,
316 lower: Index,
317 upper: Index,
318 default_value: Index,
319 long_description: &str,
320 ) -> Result<Rc<RegisteredOption>, SolverException> {
321 let mut o = RegisteredOption::new(
322 name.to_string(),
323 short_description.to_string(),
324 long_description.to_string(),
325 self.current_category.borrow().clone(),
326 self.alloc_counter(),
327 false,
328 );
329 o.option_type = OptionType::OT_Integer;
330 o.default = DefaultValue::Integer(default_value);
331 o.has_lower = true;
332 o.lower = lower as Number;
333 o.has_upper = true;
334 o.upper = upper as Number;
335 self.register(o)
336 }
337
338 pub fn add_string_option(
339 &self,
340 name: &str,
341 short_description: &str,
342 default_value: &str,
343 valid: &[(&str, &str)],
344 long_description: &str,
345 ) -> Result<Rc<RegisteredOption>, SolverException> {
346 let mut o = RegisteredOption::new(
347 name.to_string(),
348 short_description.to_string(),
349 long_description.to_string(),
350 self.current_category.borrow().clone(),
351 self.alloc_counter(),
352 false,
353 );
354 o.option_type = OptionType::OT_String;
355 o.default = DefaultValue::String(default_value.to_string());
356 o.valid_strings = valid
357 .iter()
358 .map(|(v, d)| StringEntry {
359 value: v.to_string(),
360 description: d.to_string(),
361 })
362 .collect();
363 self.register(o)
364 }
365
366 pub fn add_bool_option(
368 &self,
369 name: &str,
370 short_description: &str,
371 default_yes: bool,
372 long_description: &str,
373 ) -> Result<Rc<RegisteredOption>, SolverException> {
374 self.add_string_option(
375 name,
376 short_description,
377 if default_yes { "yes" } else { "no" },
378 &[("no", ""), ("yes", "")],
379 long_description,
380 )
381 }
382
383 pub fn get_option(&self, name: &str) -> Option<Rc<RegisteredOption>> {
387 let tag_only = match name.rfind('.') {
388 Some(pos) => &name[pos + 1..],
389 None => name,
390 };
391 self.options
392 .borrow()
393 .get(&tag_only.to_ascii_lowercase())
394 .cloned()
395 }
396
397 pub fn registered_options_in_order(&self) -> Vec<Rc<RegisteredOption>> {
399 let opts = self.options.borrow();
400 self.order
401 .borrow()
402 .iter()
403 .filter_map(|k| opts.get(k).cloned())
404 .collect()
405 }
406
407 pub fn print_options_documentation(
421 &self,
422 mode: PrintOptionsMode,
423 include_advanced: bool,
424 ) -> String {
425 let opts = self.registered_options_in_order();
426 let mut out = String::new();
427 match mode {
428 PrintOptionsMode::Text => {}
429 PrintOptionsMode::Latex => {
430 out.push_str("% pounce: latex output for print_options_mode is not yet implemented; falling through to plain text.\n\n");
431 }
432 PrintOptionsMode::Doxygen => {
433 out.push_str("<!-- pounce: doxygen output for print_options_mode is not yet implemented; falling through to plain text. -->\n\n");
434 }
435 }
436 let mut current_category: Option<String> = None;
438 for opt in opts.iter() {
439 if !include_advanced && opt.advanced {
440 continue;
441 }
442 if opt.short_description.is_empty() && opt.long_description.is_empty() {
445 continue;
446 }
447 let category = if opt.category.is_empty() {
448 "Uncategorized"
449 } else {
450 opt.category.as_str()
451 };
452 if current_category.as_deref() != Some(category) {
453 if current_category.is_some() {
454 out.push('\n');
455 }
456 out.push_str("### ");
457 out.push_str(category);
458 out.push_str(" ###\n\n");
459 current_category = Some(category.to_string());
460 }
461 format_option_text(&mut out, opt);
462 }
463 out
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
470pub enum PrintOptionsMode {
471 Text,
472 Latex,
473 Doxygen,
474}
475
476impl PrintOptionsMode {
477 pub fn from_tag(s: &str) -> Self {
481 match s.trim().to_ascii_lowercase().as_str() {
482 "latex" => Self::Latex,
483 "doxygen" => Self::Doxygen,
484 _ => Self::Text,
485 }
486 }
487}
488
489fn format_option_text(out: &mut String, opt: &RegisteredOption) {
490 out.push_str(opt.name.as_str());
491 out.push_str(": ");
492 if !opt.short_description.is_empty() {
493 out.push_str(opt.short_description.as_str());
494 }
495 out.push('\n');
496 let type_str = match opt.option_type {
497 OptionType::OT_Number => "Number",
498 OptionType::OT_Integer => "Integer",
499 OptionType::OT_String => "String",
500 OptionType::OT_Unknown => "Unknown",
501 };
502 out.push_str(" type: ");
503 out.push_str(type_str);
504 out.push('\n');
505 out.push_str(" default: ");
506 match &opt.default {
507 DefaultValue::None => out.push_str("(none)"),
508 DefaultValue::Number(v) => out.push_str(&format!("{}", v)),
509 DefaultValue::Integer(v) => out.push_str(&format!("{}", v)),
510 DefaultValue::String(v) => {
511 out.push('"');
512 out.push_str(v);
513 out.push('"');
514 }
515 }
516 out.push('\n');
517 if matches!(
518 opt.option_type,
519 OptionType::OT_Number | OptionType::OT_Integer
520 ) && (opt.has_lower || opt.has_upper)
521 {
522 out.push_str(" range: ");
523 if opt.has_lower {
524 out.push_str(&format!(
525 "{}{}",
526 if opt.lower_strict { "(" } else { "[" },
527 opt.lower
528 ));
529 } else {
530 out.push_str("(-inf");
531 }
532 out.push_str(", ");
533 if opt.has_upper {
534 out.push_str(&format!(
535 "{}{}",
536 opt.upper,
537 if opt.upper_strict { ")" } else { "]" }
538 ));
539 } else {
540 out.push_str("inf)");
541 }
542 out.push('\n');
543 }
544 if !opt.valid_strings.is_empty() {
545 out.push_str(" values:\n");
546 for entry in &opt.valid_strings {
547 if entry.description.is_empty() {
548 out.push_str(&format!(" - {}\n", entry.value));
549 } else {
550 out.push_str(&format!(" - {}: {}\n", entry.value, entry.description));
551 }
552 }
553 }
554 if !opt.long_description.is_empty() {
555 out.push('\n');
556 for line in wrap_paragraph(&opt.long_description, 76) {
557 out.push_str(" ");
558 out.push_str(&line);
559 out.push('\n');
560 }
561 }
562 out.push('\n');
563}
564
565fn wrap_paragraph(text: &str, width: usize) -> Vec<String> {
569 let mut lines = Vec::new();
570 for paragraph in text.split('\n') {
571 let mut line = String::new();
572 for word in paragraph.split_whitespace() {
573 if line.is_empty() {
574 line.push_str(word);
575 } else if line.len() + 1 + word.len() <= width {
576 line.push(' ');
577 line.push_str(word);
578 } else {
579 lines.push(std::mem::take(&mut line));
580 line.push_str(word);
581 }
582 }
583 if !line.is_empty() {
584 lines.push(line);
585 } else if paragraph.is_empty() {
586 lines.push(String::new());
587 }
588 }
589 lines
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
597 fn register_and_lookup_case_insensitive() {
598 let r = RegisteredOptions::new();
599 r.set_registering_category("Test");
600 r.add_number_option("Tol", "tolerance", 1e-8, "").unwrap();
601 assert!(r.get_option("tol").is_some());
602 assert!(r.get_option("TOL").is_some());
603 }
604
605 #[test]
606 fn print_options_documentation_renders_categories_and_metadata() {
607 let r = RegisteredOptions::new();
608 r.set_registering_category("Catty");
609 r.add_string_option(
610 "mode",
611 "How to do it.",
612 "auto",
613 &[("auto", "Decide for me."), ("manual", "I will choose.")],
614 "Long description that explains the trade-off between auto and manual selection.",
615 )
616 .unwrap();
617 r.add_bounded_number_option("tol", "Tolerance.", 0.0, true, 1.0, false, 1e-8, "")
618 .unwrap();
619 r.set_registering_category("Hidden");
620 r.add_bool_option("internal", "", false, "").unwrap();
621
622 let out = r.print_options_documentation(PrintOptionsMode::Text, false);
623 assert!(out.contains("### Catty ###"), "category header missing");
624 assert!(out.contains("mode:"), "option name missing");
625 assert!(out.contains("default: \"auto\""), "default missing");
626 assert!(
627 out.contains("- auto: Decide for me."),
628 "valid string missing"
629 );
630 assert!(out.contains("tol:"), "second option missing");
631 assert!(out.contains("range: (0, 1]"), "range formatting missing");
632 assert!(
634 !out.contains("internal:"),
635 "undocumented option leaked into output: {out}"
636 );
637
638 let latex = r.print_options_documentation(PrintOptionsMode::Latex, false);
640 assert!(latex.starts_with("% pounce: latex"));
641 assert!(latex.contains("mode:"));
642 let dox = r.print_options_documentation(PrintOptionsMode::Doxygen, false);
643 assert!(dox.starts_with("<!-- pounce: doxygen"));
644 assert!(dox.contains("mode:"));
645 }
646
647 #[test]
648 fn print_options_mode_parses_tags() {
649 assert_eq!(PrintOptionsMode::from_tag("text"), PrintOptionsMode::Text);
650 assert_eq!(PrintOptionsMode::from_tag("LaTeX"), PrintOptionsMode::Latex);
651 assert_eq!(
652 PrintOptionsMode::from_tag("doxygen"),
653 PrintOptionsMode::Doxygen
654 );
655 assert_eq!(PrintOptionsMode::from_tag("html"), PrintOptionsMode::Text);
657 }
658
659 #[test]
660 fn duplicate_registration_is_error() {
661 let r = RegisteredOptions::new();
662 r.add_number_option("alpha", "", 1.0, "").unwrap();
663 let err = r.add_number_option("ALPHA", "", 2.0, "").unwrap_err();
664 assert_eq!(err.kind, ExceptionKind::OPTION_ALREADY_REGISTERED);
665 }
666
667 #[test]
668 fn bounds_check_on_number() {
669 let r = RegisteredOptions::new();
670 r.add_lower_bounded_number_option("mu", "", 0.0, true, 0.1, "")
671 .unwrap();
672 let opt = r.get_option("mu").unwrap();
673 assert!(opt.is_valid_number(1e-12));
674 assert!(!opt.is_valid_number(0.0));
675 assert!(!opt.is_valid_number(-1.0));
676 }
677
678 #[test]
679 fn string_enum_lookup() {
680 let r = RegisteredOptions::new();
681 r.add_string_option(
682 "linear_solver",
683 "",
684 "mumps",
685 &[("mumps", "MUMPS"), ("feral", "FERAL")],
686 "",
687 )
688 .unwrap();
689 let opt = r.get_option("linear_solver").unwrap();
690 assert!(opt.is_valid_string("MuMpS"));
691 assert!(!opt.is_valid_string("ma27"));
692 assert_eq!(opt.map_string_to_enum("feral"), Some(1));
693 }
694
695 #[test]
696 fn registration_order_preserved() {
697 let r = RegisteredOptions::new();
698 r.add_number_option("c", "", 0.0, "").unwrap();
699 r.add_number_option("a", "", 0.0, "").unwrap();
700 r.add_number_option("b", "", 0.0, "").unwrap();
701 let order: Vec<_> = r
702 .registered_options_in_order()
703 .iter()
704 .map(|o| o.name.clone())
705 .collect();
706 assert_eq!(order, vec!["c", "a", "b"]);
707 }
708}