1use std::collections::HashMap;
12use std::fmt;
13use std::path::Path;
14
15use serde_json::Value;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum OptionType {
24 String,
25 Integer,
26 Float,
27 Boolean,
28 List,
29 Enum,
30 Path,
31 Size,
32}
33
34impl fmt::Display for OptionType {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::String => write!(f, "string"),
38 Self::Integer => write!(f, "integer"),
39 Self::Float => write!(f, "float"),
40 Self::Boolean => write!(f, "boolean"),
41 Self::List => write!(f, "list"),
42 Self::Enum => write!(f, "enum"),
43 Self::Path => write!(f, "path"),
44 Self::Size => write!(f, "size"),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum OptionCategory {
52 General,
53 HttpFtp,
54 BitTorrent,
55 Rpc,
56 Advanced,
57}
58
59impl fmt::Display for OptionCategory {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::General => write!(f, "general"),
63 Self::HttpFtp => write!(f, "http/ftp"),
64 Self::BitTorrent => write!(f, "bittorrent"),
65 Self::Rpc => write!(f, "rpc"),
66 Self::Advanced => write!(f, "advanced"),
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Default)]
73pub enum OptionValue {
74 Bool(bool),
75 Int(i64),
76 Usize(usize),
77 Float(f64),
78 Str(String),
79 List(Vec<String>),
80 #[default]
81 None,
82}
83
84impl fmt::Display for OptionValue {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self {
87 Self::Bool(b) => write!(f, "{}", b),
88 Self::Int(n) => write!(f, "{}", n),
89 Self::Usize(n) => write!(f, "{}", n),
90 Self::Float(v) => write!(f, "{}", v),
91 Self::Str(s) => write!(f, "{}", s),
92 Self::List(items) => write!(f, "{}", items.join(",")),
93 Self::None => write!(f, ""),
94 }
95 }
96}
97
98impl From<&OptionValue> for serde_json::Value {
99 fn from(v: &OptionValue) -> Self {
100 match v {
101 OptionValue::Bool(b) => serde_json::json!(*b),
102 OptionValue::Int(n) => serde_json::json!(*n),
103 OptionValue::Usize(n) => serde_json::json!(*n),
104 OptionValue::Float(v) => serde_json::json!(*v),
105 OptionValue::Str(s) => serde_json::json!(s),
106 OptionValue::List(items) => serde_json::json!(items),
107 OptionValue::None => serde_json::Value::Null,
108 }
109 }
110}
111
112impl From<serde_json::Value> for OptionValue {
113 fn from(val: serde_json::Value) -> Self {
114 match val {
115 serde_json::Value::Bool(b) => Self::Bool(b),
116 serde_json::Value::Number(n) if n.is_i64() => Self::Int(n.as_i64().unwrap()),
117 serde_json::Value::Number(n) if n.is_f64() => Self::Float(n.as_f64().unwrap()),
118 serde_json::Value::Number(n) if n.is_u64() => Self::Usize(n.as_u64().unwrap() as usize),
119 serde_json::Value::String(s) => Self::Str(s),
120 serde_json::Value::Array(arr) => Self::List(
121 arr.iter()
122 .filter_map(|v| v.as_str().map(String::from))
123 .collect(),
124 ),
125 _ => Self::None,
126 }
127 }
128}
129
130impl OptionValue {
131 pub fn as_str(&self) -> Option<&str> {
132 if let Self::Str(s) = self {
133 Some(s)
134 } else {
135 None
136 }
137 }
138 pub fn as_i64(&self) -> Option<i64> {
139 if let Self::Int(n) = self {
140 Some(*n)
141 } else {
142 None
143 }
144 }
145 pub fn as_f64(&self) -> Option<f64> {
146 if let Self::Float(v) = self {
147 Some(*v)
148 } else {
149 None
150 }
151 }
152 pub fn as_bool(&self) -> Option<bool> {
153 if let Self::Bool(b) = self {
154 Some(*b)
155 } else {
156 None
157 }
158 }
159 pub fn as_list(&self) -> Option<&Vec<String>> {
160 if let Self::List(l) = self {
161 Some(l)
162 } else {
163 None
164 }
165 }
166 pub fn as_usize(&self) -> usize {
167 match self {
168 Self::Usize(n) => *n,
169 _ => 0,
170 }
171 }
172 pub fn as_str_vec(&self) -> &[String] {
173 match self {
174 Self::List(v) => v.as_slice(),
175 _ => &[],
176 }
177 }
178 pub fn is_none(&self) -> bool {
179 matches!(self, Self::None)
180 }
181
182 pub fn parse_size_str(s: &str) -> u64 {
183 let s = s.trim();
184 let (num_part, suffix) = if s.len() > 1 {
185 let last_char = s.chars().last().unwrap();
186 match last_char {
187 'K' | 'k' => (&s[..s.len() - 1], 1024u64),
188 'M' | 'm' => (&s[..s.len() - 1], 1024 * 1024),
189 'G' | 'g' => (&s[..s.len() - 1], 1024u64 * 1024 * 1024),
190 'T' | 't' => (&s[..s.len() - 1], 1024u64 * 1024 * 1024 * 1024),
191 _ => (s, 1u64),
192 }
193 } else {
194 (s, 1u64)
195 };
196 num_part
197 .parse::<f64>()
198 .map(|n| (n * suffix as f64) as u64)
199 .unwrap_or(0)
200 }
201
202 pub fn to_size_string(bytes: u64) -> String {
203 const K: u64 = 1024;
204 const M: u64 = K * K;
205 const G: u64 = M * K;
206 const T: u64 = G * K;
207 if bytes >= T {
208 format!("{}T", bytes as f64 / T as f64)
209 } else if bytes >= G {
210 format!("{}G", bytes as f64 / G as f64)
211 } else if bytes >= M {
212 format!("{}M", bytes as f64 / M as f64)
213 } else if bytes >= K {
214 format!("{}K", bytes as f64 / K as f64)
215 } else {
216 format!("{}", bytes)
217 }
218 }
219}
220
221#[derive(Debug, Clone)]
223pub struct OptionDef {
224 pub name: String,
225 pub short_name: Option<char>,
226 pub opt_type: OptionType,
227 pub default_value: OptionValue,
228 pub description: String,
229 pub category: OptionCategory,
230 pub min: Option<i64>,
231 pub max: Option<u64>,
232 pub deprecated: bool,
233 pub hidden: bool,
234}
235
236impl Default for OptionDef {
237 fn default() -> Self {
238 Self {
239 name: String::new(),
240 short_name: None,
241 opt_type: OptionType::String,
242 default_value: OptionValue::None,
243 description: String::new(),
244 category: OptionCategory::General,
245 min: None,
246 max: None,
247 deprecated: false,
248 hidden: false,
249 }
250 }
251}
252
253impl OptionDef {
254 pub fn new(name: impl Into<String>, opt_type: OptionType) -> Self {
255 Self {
256 name: name.into(),
257 opt_type,
258 ..Default::default()
259 }
260 }
261
262 pub fn name(&self) -> &str {
263 &self.name
264 }
265 pub fn short_name(&self) -> Option<char> {
266 self.short_name
267 }
268 pub fn opt_type(&self) -> OptionType {
269 self.opt_type
270 }
271 pub fn default_value(&self) -> &OptionValue {
272 &self.default_value
273 }
274 pub fn get_category(&self) -> OptionCategory {
275 self.category
276 }
277 pub fn is_deprecated(&self) -> bool {
278 self.deprecated
279 }
280 pub fn is_hidden(&self) -> bool {
281 self.hidden
282 }
283
284 pub fn description(&self) -> &str {
285 &self.description
286 }
287
288 pub fn parse_value(&self, s: &str) -> Result<OptionValue, String> {
289 if s.is_empty() {
290 return Ok(self.default_value.clone());
291 }
292 match self.opt_type {
293 OptionType::String | OptionType::Path | OptionType::Enum => {
294 Ok(OptionValue::Str(s.to_string()))
295 }
296 OptionType::Integer => s
297 .parse::<i64>()
298 .map(|n| {
299 if let Some(min) = self.min
300 && n < min
301 {
302 return Err(format!("value {} < minimum {}", n, min));
303 }
304 if let Some(max) = self.max
305 && (n < 0 || n as u64 > max)
306 {
307 return Err(format!("value {} exceeds maximum {}", n, max));
308 }
309 Ok(OptionValue::Int(n))
310 })
311 .map_err(|e| format!("invalid integer '{}': {}", s, e))?,
312 OptionType::Size => Ok(OptionValue::Int(OptionValue::parse_size_str(s) as i64)),
313 OptionType::Float => s
314 .parse::<f64>()
315 .map(OptionValue::Float)
316 .map_err(|e| format!("invalid float '{}': {}", s, e)),
317 OptionType::Boolean => match s.to_lowercase().as_str() {
318 "true" | "yes" | "1" | "on" => Ok(OptionValue::Bool(true)),
319 "false" | "no" | "0" | "off" => Ok(OptionValue::Bool(false)),
320 _ => Err(format!("invalid boolean '{}'", s)),
321 },
322 OptionType::List => Ok(OptionValue::List(
323 s.split(',').map(|x| x.trim().to_string()).collect(),
324 )),
325 }
326 }
327}
328
329#[derive(Debug, Clone)]
335pub enum OptionError {
336 TypeMismatch {
337 expected: String,
338 got: String,
339 },
340 OutOfRange {
341 value: String,
342 min: String,
343 max: String,
344 },
345 InvalidChoice {
346 value: String,
347 allowed: Vec<String>,
348 },
349 InvalidUrl {
350 url: String,
351 reason: String,
352 },
353 InvalidPath {
354 path: String,
355 reason: String,
356 },
357 PatternMismatch {
358 value: String,
359 pattern: String,
360 },
361 DependencyConflict {
362 option: String,
363 conflicts_with: String,
364 },
365 MissingDependency {
366 option: String,
367 requires: String,
368 },
369}
370
371impl fmt::Display for OptionError {
372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373 match self {
374 Self::TypeMismatch { expected, got } => {
375 write!(
376 f,
377 "type mismatch for option: expected '{}', got '{}'",
378 expected, got
379 )
380 }
381 Self::OutOfRange { value, min, max } => {
382 write!(f, "value '{}' is out of range [{}..{}]", value, min, max)
383 }
384 Self::InvalidChoice { value, allowed } => {
385 write!(
386 f,
387 "invalid choice '{}', allowed values: {}",
388 value,
389 allowed.join(", ")
390 )
391 }
392 Self::InvalidUrl { url, reason } => {
393 write!(f, "invalid URL '{}': {}", url, reason)
394 }
395 Self::InvalidPath { path, reason } => {
396 write!(f, "invalid path '{}': {}", path, reason)
397 }
398 Self::PatternMismatch { value, pattern } => {
399 write!(f, "value '{}' does not match pattern '{}'", value, pattern)
400 }
401 Self::DependencyConflict {
402 option,
403 conflicts_with,
404 } => {
405 write!(f, "option '{}' conflicts with '{}'", option, conflicts_with)
406 }
407 Self::MissingDependency { option, requires } => {
408 write!(f, "option '{}' requires '{}' to be set", option, requires)
409 }
410 }
411 }
412}
413
414impl std::error::Error for OptionError {}
415
416pub trait OptionValidator: Send + Sync {
418 fn validate(&self, name: &str, value: &Value) -> Result<(), OptionError>;
419 fn description(&self) -> &str;
420}
421
422#[derive(Debug, Clone)]
424pub struct RangeValidator<T> {
425 min: T,
426 max: T,
427}
428
429impl<T> RangeValidator<T>
430where
431 T: PartialOrd + fmt::Display + Clone + 'static,
432{
433 pub fn new(min: T, max: T) -> Self {
434 Self { min, max }
435 }
436}
437
438impl OptionValidator for RangeValidator<i64> {
439 fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
440 match value.as_i64() {
441 Some(n) if n >= self.min && n <= self.max => Ok(()),
442 Some(n) => Err(OptionError::OutOfRange {
443 value: n.to_string(),
444 min: self.min.to_string(),
445 max: self.max.to_string(),
446 }),
447 None => Err(OptionError::TypeMismatch {
448 expected: "integer".to_string(),
449 got: format!("{:?}", value),
450 }),
451 }
452 }
453
454 fn description(&self) -> &str {
455 "range validator (inclusive bounds)"
456 }
457}
458
459impl OptionValidator for RangeValidator<f64> {
460 fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
461 match value.as_f64() {
462 Some(v) if v >= self.min && v <= self.max => Ok(()),
463 Some(v) => Err(OptionError::OutOfRange {
464 value: format!("{}", v),
465 min: format!("{}", self.min),
466 max: format!("{}", self.max),
467 }),
468 None => Err(OptionError::TypeMismatch {
469 expected: "float".to_string(),
470 got: format!("{:?}", value),
471 }),
472 }
473 }
474
475 fn description(&self) -> &str {
476 "range validator for floating-point numbers"
477 }
478}
479
480impl OptionValidator for RangeValidator<u64> {
481 fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
482 match value.as_u64() {
483 Some(n) if n >= self.min && n <= self.max => Ok(()),
484 Some(n) => Err(OptionError::OutOfRange {
485 value: n.to_string(),
486 min: self.min.to_string(),
487 max: self.max.to_string(),
488 }),
489 None => Err(OptionError::TypeMismatch {
490 expected: "unsigned integer".to_string(),
491 got: format!("{:?}", value),
492 }),
493 }
494 }
495
496 fn description(&self) -> &str {
497 "range validator for unsigned integers"
498 }
499}
500
501#[derive(Debug, Clone)]
503pub struct ChoiceValidator {
504 allowed: Vec<String>,
505}
506
507impl ChoiceValidator {
508 pub fn new(allowed: Vec<String>) -> Self {
509 Self { allowed }
510 }
511
512 pub fn allowed_values(&self) -> &[String] {
513 &self.allowed
514 }
515}
516
517impl OptionValidator for ChoiceValidator {
518 fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
519 match value.as_str() {
520 Some(s) if self.allowed.iter().any(|a| a == s) => Ok(()),
521 Some(s) => Err(OptionError::InvalidChoice {
522 value: s.to_string(),
523 allowed: self.allowed.clone(),
524 }),
525 None => Err(OptionError::TypeMismatch {
526 expected: "string".to_string(),
527 got: format!("{:?}", value),
528 }),
529 }
530 }
531
532 fn description(&self) -> &str {
533 "choice validator (enum whitelist)"
534 }
535}
536
537#[derive(Debug, Clone, Copy)]
539pub struct UrlValidator;
540
541impl UrlValidator {
542 pub fn new() -> Self {
543 Self
544 }
545
546 fn is_valid_url(url: &str) -> Result<(), String> {
547 if url.is_empty() {
548 return Err("URL is empty".to_string());
549 }
550
551 if !url.contains("://") {
552 return Err(format!("missing scheme separator in '{}'", url));
553 }
554
555 let scheme = url.split("://").next().unwrap_or("");
556 if scheme.is_empty() {
557 return Err("scheme is empty".to_string());
558 }
559
560 if !scheme
561 .chars()
562 .all(|c| c.is_alphanumeric() || c == '+' || c == '-' || c == '.')
563 {
564 return Err(format!("invalid scheme '{}'", scheme));
565 }
566
567 let after_scheme = url.split_once("://").map(|x| x.1).unwrap_or("");
568 if after_scheme.is_empty() {
569 return Err("no host/path after scheme".to_string());
570 }
571
572 Ok(())
573 }
574}
575
576impl Default for UrlValidator {
577 fn default() -> Self {
578 Self::new()
579 }
580}
581
582impl OptionValidator for UrlValidator {
583 fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
584 match value.as_str() {
585 Some(url) => Self::is_valid_url(url).map_err(|reason| OptionError::InvalidUrl {
586 url: url.to_string(),
587 reason,
588 }),
589 None => Err(OptionError::TypeMismatch {
590 expected: "string (URL)".to_string(),
591 got: format!("{:?}", value),
592 }),
593 }
594 }
595
596 fn description(&self) -> &str {
597 "URL format validator"
598 }
599}
600
601#[derive(Debug, Clone)]
603pub struct PathValidator {
604 must_exist: bool,
605 writable: bool,
606}
607
608impl PathValidator {
609 pub fn new(must_exist: bool, writable: bool) -> Self {
610 Self {
611 must_exist,
612 writable,
613 }
614 }
615}
616
617impl OptionValidator for PathValidator {
618 fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
619 match value.as_str() {
620 Some(path_str) => {
621 let path = Path::new(path_str);
622
623 if self.must_exist && !path.exists() {
624 return Err(OptionError::InvalidPath {
625 path: path_str.to_string(),
626 reason: "path does not exist".to_string(),
627 });
628 }
629
630 if self.writable {
631 let check_path = if path.exists() {
632 path.as_os_str().to_owned()
633 } else {
634 match path.parent() {
635 Some(parent) if !parent.as_os_str().is_empty() => {
636 parent.as_os_str().to_owned()
637 }
638 _ => path.as_os_str().to_owned(),
639 }
640 };
641
642 if let Some(check_path_str) = check_path.to_str() {
643 let check_path = Path::new(check_path_str);
644 if check_path.exists() {
645 match std::fs::metadata(check_path) {
646 Ok(meta) => {
647 #[cfg(unix)]
648 {
649 use std::os::unix::fs::PermissionsExt;
650 let mode = meta.permissions().mode();
651 let user_writable = mode & 0o200 != 0;
652 if !user_writable && meta.is_dir() {
653 return Err(OptionError::InvalidPath {
654 path: path_str.to_string(),
655 reason: "path is not writable".to_string(),
656 });
657 }
658 }
659 #[cfg(not(unix))]
660 {
661 let _ = &meta;
662 }
663 }
664 Err(e) => {
665 return Err(OptionError::InvalidPath {
666 path: path_str.to_string(),
667 reason: format!("cannot access path: {}", e),
668 });
669 }
670 }
671 }
672 }
673 }
674
675 Ok(())
676 }
677 None => Err(OptionError::TypeMismatch {
678 expected: "string (path)".to_string(),
679 got: format!("{:?}", value),
680 }),
681 }
682 }
683
684 fn description(&self) -> &str {
685 "file system path validator"
686 }
687}
688
689#[derive(Debug, Clone)]
691pub struct RegexValidator {
692 pattern: String,
693 compiled: regex::Regex,
694}
695
696impl RegexValidator {
697 pub fn new(pattern: &str) -> Self {
698 let compiled = regex::Regex::new(pattern).expect("Invalid regex pattern in RegexValidator");
699 Self {
700 pattern: pattern.to_string(),
701 compiled,
702 }
703 }
704
705 pub fn pattern(&self) -> &str {
706 &self.pattern
707 }
708}
709
710impl OptionValidator for RegexValidator {
711 fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
712 match value.as_str() {
713 Some(s) if self.compiled.is_match(s) => Ok(()),
714 Some(s) => Err(OptionError::PatternMismatch {
715 value: s.to_string(),
716 pattern: self.pattern.clone(),
717 }),
718 None => Err(OptionError::TypeMismatch {
719 expected: "string".to_string(),
720 got: format!("{:?}", value),
721 }),
722 }
723 }
724
725 fn description(&self) -> &str {
726 "custom regex pattern validator"
727 }
728}
729
730pub struct OptionDefinition {
732 pub name: &'static str,
733 pub description: &'static str,
734 pub default_value: Value,
735 pub validator: Option<Box<dyn OptionValidator>>,
736}
737
738impl OptionDefinition {
739 pub fn new(name: &'static str, description: &'static str, default_value: Value) -> Self {
740 Self {
741 name,
742 description,
743 default_value,
744 validator: None,
745 }
746 }
747
748 pub fn with_validator(mut self, validator: Box<dyn OptionValidator>) -> Self {
749 self.validator = Some(validator);
750 self
751 }
752
753 pub fn validate(&self, value: &Value) -> Result<(), OptionError> {
754 match &self.validator {
755 Some(validator) => validator.validate(self.name, value),
756 None => Ok(()),
757 }
758 }
759
760 pub fn get_default_or_fallback<'a>(&'a self, fallback: &'a Value) -> &'a Value {
761 match &self.default_value {
762 Value::Null => fallback,
763 other => other,
764 }
765 }
766}
767
768pub struct DependencyChecker {
770 mutual_exclusions: Vec<(String, String)>,
771 requirements: Vec<(String, String)>,
772}
773
774impl DependencyChecker {
775 pub fn new() -> Self {
776 Self {
777 mutual_exclusions: Vec::new(),
778 requirements: Vec::new(),
779 }
780 }
781
782 pub fn add_mutual_exclusion(&mut self, opt_a: String, opt_b: String) {
783 self.mutual_exclusions.push((opt_a, opt_b));
784 }
785
786 pub fn add_requirement(&mut self, option: String, requires: String) {
787 self.requirements.push((option, requires));
788 }
789
790 pub fn check(&self, options: &HashMap<String, Value>) -> Vec<OptionError> {
791 let mut errors = Vec::new();
792
793 for (opt_a, opt_b) in &self.mutual_exclusions {
794 let a_set = options.get(opt_a).is_some_and(|v| !v.is_null());
795 let b_set = options.get(opt_b).is_some_and(|v| !v.is_null());
796
797 if a_set && b_set {
798 errors.push(OptionError::DependencyConflict {
799 option: opt_a.clone(),
800 conflicts_with: opt_b.clone(),
801 });
802 }
803 }
804
805 for (option, requires) in &self.requirements {
806 let option_set = options.get(option).is_some_and(|v| !v.is_null());
807 let required_set = options.get(requires).is_some_and(|v| !v.is_null());
808
809 if option_set && !required_set {
810 errors.push(OptionError::MissingDependency {
811 option: option.clone(),
812 requires: requires.clone(),
813 });
814 }
815 }
816
817 errors
818 }
819
820 pub fn mutual_exclusion_count(&self) -> usize {
821 self.mutual_exclusions.len()
822 }
823
824 pub fn requirement_count(&self) -> usize {
825 self.requirements.len()
826 }
827}
828
829impl Default for DependencyChecker {
830 fn default() -> Self {
831 Self::new()
832 }
833}
834
835#[derive(Clone)]
841pub struct OptionRegistry {
842 options: HashMap<String, OptionDef>,
843}
844
845impl OptionRegistry {
846 pub fn new() -> Self {
847 let mut reg = Self {
848 options: HashMap::new(),
849 };
850 reg.register_general_options();
851 reg.register_http_ftp_options();
852 reg.register_bt_options();
853 reg.register_rpc_options();
854 reg.register_advanced_options();
855 reg
856 }
857
858 pub fn register(&mut self, def: OptionDef) {
859 self.options.insert(def.name().to_string(), def);
860 }
861
862 pub fn get(&self, name: &str) -> Option<&OptionDef> {
863 self.options.get(name)
864 }
865
866 pub fn contains(&self, name: &str) -> bool {
867 self.options.contains_key(name)
868 }
869
870 pub fn all(&self) -> &HashMap<String, OptionDef> {
871 &self.options
872 }
873
874 pub fn count(&self) -> usize {
875 self.options.len()
876 }
877
878 pub fn by_category(&self, cat: OptionCategory) -> Vec<&OptionDef> {
879 self.options
880 .values()
881 .filter(|d| d.get_category() == cat)
882 .collect()
883 }
884}
885
886impl Default for OptionRegistry {
887 fn default() -> Self {
888 Self::new()
889 }
890}
891
892#[cfg(test)]
893mod tests {
894 use super::*;
895
896 #[test]
897 fn test_option_type_display() {
898 assert_eq!(OptionType::String.to_string(), "string");
899 assert_eq!(OptionType::Boolean.to_string(), "boolean");
900 assert_eq!(OptionType::Size.to_string(), "size");
901 }
902
903 #[test]
904 fn test_option_category_display() {
905 assert_eq!(OptionCategory::General.to_string(), "general");
906 assert_eq!(OptionCategory::BitTorrent.to_string(), "bittorrent");
907 }
908
909 #[test]
910 fn test_option_value_variants() {
911 let s = OptionValue::Str("hello".into());
912 assert_eq!(s.as_str().unwrap(), "hello");
913
914 let n = OptionValue::Int(42);
915 assert_eq!(n.as_i64().unwrap(), 42);
916
917 let b = OptionValue::Bool(true);
918 assert!(b.as_bool().unwrap());
919
920 let l = OptionValue::List(vec!["a".into(), "b".into()]);
921 assert_eq!(l.as_list().unwrap().len(), 2);
922
923 let none = OptionValue::None;
924 assert!(none.is_none());
925 }
926
927 #[test]
928 fn test_option_value_display() {
929 assert_eq!(OptionValue::Str("test".into()).to_string(), "test");
930 assert_eq!(OptionValue::Int(99).to_string(), "99");
931 assert_eq!(OptionValue::Bool(true).to_string(), "true");
932 assert_eq!(
933 OptionValue::List(vec!["x".into(), "y".into()]).to_string(),
934 "x,y"
935 );
936 }
937
938 #[test]
939 fn test_option_value_to_json() {
940 let v = OptionValue::Str("hello".into());
941 let jv: serde_json::Value = (&v).into();
942 assert_eq!(jv, "hello");
943
944 let v2 = OptionValue::Int(123);
945 let jv2: serde_json::Value = (&v2).into();
946 assert_eq!(jv2, 123);
947
948 let v3 = OptionValue::Bool(false);
949 let jv3: serde_json::Value = (&v3).into();
950 assert_eq!(jv3, false);
951
952 let v4 = OptionValue::List(vec!["a".into()]);
953 let jv4: serde_json::Value = (&v4).into();
954 assert!(jv4.is_array());
955 }
956
957 #[test]
958 fn test_option_value_from_json() {
959 let ov: OptionValue = serde_json::json!("test string").into();
960 assert_eq!(ov.as_str().unwrap(), "test string");
961
962 let ov2: OptionValue = serde_json::json!(42).into();
963 assert_eq!(ov2.as_i64().unwrap(), 42);
964
965 let ov3: OptionValue = serde_json::json!(true).into();
966 assert!(ov3.as_bool().unwrap());
967
968 let ov4: OptionValue = serde_json::json!(["a", "b"]).into();
969 assert_eq!(ov4.as_list().unwrap().len(), 2);
970 }
971
972 #[test]
973 fn test_size_parsing() {
974 assert_eq!(OptionValue::parse_size_str("100"), 100);
975 assert_eq!(OptionValue::parse_size_str("1K"), 1024);
976 assert_eq!(OptionValue::parse_size_str("2M"), 2 * 1024 * 1024);
977 assert_eq!(OptionValue::parse_size_str("1G"), 1024u64 * 1024 * 1024);
978 assert_eq!(OptionValue::parse_size_str("0"), 0);
979 }
980
981 #[test]
982 fn test_size_display() {
983 assert!(OptionValue::to_size_string(500).contains("500"));
984 assert!(OptionValue::to_size_string(2048).contains("K"));
985 assert!(OptionValue::to_size_string(3 * 1024 * 1024).contains("M"));
986 }
987
988 #[test]
989 fn test_option_def_builder() {
990 let def = OptionDef {
991 name: "split".into(),
992 opt_type: OptionType::Integer,
993 short_name: Some('s'),
994 default_value: OptionValue::Int(5),
995 description: "Connections per download".into(),
996 min: Some(1),
997 max: Some(16),
998 category: OptionCategory::HttpFtp,
999 ..Default::default()
1000 };
1001 assert_eq!(def.name(), "split");
1002 assert_eq!(def.short_name(), Some('s'));
1003 assert_eq!(def.opt_type(), OptionType::Integer);
1004 assert!(!def.is_deprecated());
1005 assert!(!def.is_hidden());
1006 }
1007
1008 #[test]
1009 fn test_option_def_parse_integer() {
1010 let def = OptionDef {
1011 name: "split".into(),
1012 opt_type: OptionType::Integer,
1013 min: Some(1),
1014 max: Some(16),
1015 ..Default::default()
1016 };
1017 let v = def.parse_value("5").unwrap();
1018 assert_eq!(v.as_i64().unwrap(), 5);
1019
1020 let err = def.parse_value("0");
1021 assert!(err.is_err());
1022
1023 let err2 = def.parse_value("abc");
1024 assert!(err2.is_err());
1025 }
1026
1027 #[test]
1028 fn test_option_def_parse_boolean() {
1029 let def = OptionDef::new("verbose", OptionType::Boolean);
1030 assert!(def.parse_value("true").unwrap().as_bool().unwrap());
1031 assert!(def.parse_value("yes").unwrap().as_bool().unwrap());
1032 assert!(def.parse_value("1").unwrap().as_bool().unwrap());
1033 assert!(!def.parse_value("false").unwrap().as_bool().unwrap());
1034 assert!(!def.parse_value("no").unwrap().as_bool().unwrap());
1035 assert!(def.parse_value("invalid").is_err());
1036 }
1037
1038 #[test]
1039 fn test_option_def_parse_list() {
1040 let def = OptionDef::new("header", OptionType::List);
1041 let v = def.parse_value("X-Custom:foo,X-Bar:baz").unwrap();
1042 assert_eq!(v.as_list().unwrap().len(), 2);
1043 }
1044
1045 #[test]
1046 fn test_option_def_parse_empty_uses_default() {
1047 let def = OptionDef {
1048 name: "dir".into(),
1049 opt_type: OptionType::Path,
1050 default_value: OptionValue::Str("/tmp".into()),
1051 ..Default::default()
1052 };
1053 let v = def.parse_value("").unwrap();
1054 assert_eq!(v.as_str().unwrap(), "/tmp");
1055 }
1056
1057 #[test]
1058 fn test_registry_creation() {
1059 let reg = OptionRegistry::new();
1060 assert!(reg.count() >= 60);
1061 assert!(reg.get("split").is_some());
1062 assert!(reg.get("nonexistent-option").is_none());
1063 }
1064
1065 #[test]
1066 fn test_registry_by_category() {
1067 let reg = OptionRegistry::new();
1068 let general = reg.by_category(OptionCategory::General);
1069 let bt = reg.by_category(OptionCategory::BitTorrent);
1070 let rpc = reg.by_category(OptionCategory::Rpc);
1071 assert!(!general.is_empty());
1072 assert!(!bt.is_empty());
1073 assert!(!rpc.is_empty());
1074 }
1075
1076 #[test]
1077 fn test_registry_defaults_are_valid() {
1078 let reg = OptionRegistry::new();
1079 for def in reg.all().values() {
1080 if !matches!(def.default_value(), OptionValue::None) {
1081 let parsed = def.parse_value(&def.default_value().to_string());
1082 assert!(
1083 parsed.is_ok(),
1084 "Default value for '{}' failed to re-parse: {:?}",
1085 def.name(),
1086 parsed.err()
1087 );
1088 }
1089 }
1090 }
1091
1092 #[test]
1093 fn test_default_registry() {
1094 let reg = OptionRegistry::default();
1095 assert!(reg.count() > 0);
1096 }
1097
1098 #[test]
1101 fn test_range_validator_in_range() {
1102 let validator = RangeValidator::<i64>::new(1, 16);
1103 assert!(validator.validate("split", &Value::from(1)).is_ok());
1104 assert!(validator.validate("split", &Value::from(8)).is_ok());
1105 assert!(validator.validate("split", &Value::from(16)).is_ok());
1106
1107 let float_validator = RangeValidator::<f64>::new(0.0, 1.0);
1108 assert!(float_validator.validate("ratio", &Value::from(0.5)).is_ok());
1109
1110 let u64_validator = RangeValidator::<u64>::new(1024, 1024 * 1024);
1111 assert!(
1112 u64_validator
1113 .validate("size", &Value::from(4096u64))
1114 .is_ok()
1115 );
1116 }
1117
1118 #[test]
1119 fn test_range_validator_out_of_range() {
1120 let validator = RangeValidator::<i64>::new(1, 16);
1121 let result = validator.validate("split", &Value::from(0));
1122 assert!(result.is_err());
1123 match result.unwrap_err() {
1124 OptionError::OutOfRange { value, min, max } => {
1125 assert_eq!(value, "0");
1126 assert_eq!(min, "1");
1127 assert_eq!(max, "16");
1128 }
1129 other => panic!("Expected OutOfRange error, got {:?}", other),
1130 }
1131 }
1132
1133 #[test]
1134 fn test_choice_validator_enum() {
1135 let validator = ChoiceValidator::new(vec![
1136 "debug".to_string(),
1137 "info".to_string(),
1138 "warn".to_string(),
1139 "error".to_string(),
1140 ]);
1141 assert!(
1142 validator
1143 .validate("log-level", &Value::String("debug".into()))
1144 .is_ok()
1145 );
1146 assert!(
1147 validator
1148 .validate("log-level", &Value::String("verbose".into()))
1149 .is_err()
1150 );
1151 }
1152
1153 #[test]
1154 fn test_url_validator_malformed() {
1155 let validator = UrlValidator::new();
1156 assert!(
1157 validator
1158 .validate(
1159 "tracker",
1160 &Value::String("http://example.com:6969/announce".into())
1161 )
1162 .is_ok()
1163 );
1164 assert!(
1165 validator
1166 .validate("url", &Value::String("not-a-url".into()))
1167 .is_err()
1168 );
1169 }
1170
1171 #[test]
1172 fn test_regex_validator_pattern_match() {
1173 let validator = RegexValidator::new(r"^[a-zA-Z0-9.-]+:\d+$");
1174 assert!(
1175 validator
1176 .validate("proxy", &Value::String("proxy.example.com:8080".into()))
1177 .is_ok()
1178 );
1179 assert!(
1180 validator
1181 .validate("proxy", &Value::String("not-valid".into()))
1182 .is_err()
1183 );
1184 }
1185
1186 #[test]
1187 fn test_dependency_checker() {
1188 let mut checker = DependencyChecker::new();
1189 checker.add_mutual_exclusion("ftp-pasv".to_string(), "ftp-port".to_string());
1190 checker.add_requirement("bt-enable-lpd".to_string(), "enable-dht".to_string());
1191
1192 let mut opts = HashMap::new();
1193 opts.insert("ftp-pasv".to_string(), Value::Bool(true));
1194 opts.insert("ftp-port".to_string(), Value::from(8021));
1195 let errors = checker.check(&opts);
1196 assert_eq!(errors.len(), 1);
1197 }
1198
1199 #[test]
1200 fn test_option_definition_validation() {
1201 let def = OptionDefinition {
1202 name: "max-connections",
1203 description: "Maximum connections per server",
1204 default_value: Value::from(16),
1205 validator: Some(Box::new(RangeValidator::<i64>::new(1, 32))),
1206 };
1207 assert!(def.validate(&Value::from(8)).is_ok());
1208 assert!(def.validate(&Value::from(0)).is_err());
1209 }
1210
1211 #[test]
1212 fn test_option_error_display() {
1213 let err = OptionError::TypeMismatch {
1214 expected: "integer".to_string(),
1215 got: "string".to_string(),
1216 };
1217 let msg = format!("{}", err);
1218 assert!(msg.contains("type mismatch"));
1219 }
1220}