1use chrono::Local;
6use serde::{Deserialize, Serialize};
7
8use super::color::Colors;
9use super::error::{Result, TwygError};
10use super::level::LogLevel;
11use super::output::Output;
12use super::timestamp::TSFormat;
13
14const DEFAULT_TS_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
18pub enum PadSide {
19 Left,
21
22 #[default]
24 Right,
25}
26
27#[derive(Clone, Debug, Serialize, Deserialize)]
46pub struct Opts {
47 #[serde(default)]
49 coloured: bool,
50
51 #[serde(default)]
53 output: Output,
54
55 #[serde(default)]
57 level: LogLevel,
58
59 #[serde(default)]
61 report_caller: bool,
62
63 #[serde(default)]
65 timestamp_format: TSFormat,
66
67 #[serde(default)]
69 pad_level: bool,
70
71 #[serde(default = "default_pad_amount")]
73 pad_amount: usize,
74
75 #[serde(default)]
77 pad_side: PadSide,
78
79 #[serde(default = "default_msg_separator")]
81 msg_separator: String,
82
83 #[serde(default = "default_arrow_char")]
85 arrow_char: String,
86
87 #[serde(default)]
89 colors: Colors,
90}
91
92fn default_pad_amount() -> usize {
94 5
95}
96
97fn default_msg_separator() -> String {
98 ": ".to_string()
99}
100
101fn default_arrow_char() -> String {
102 "▶".to_string()
103}
104
105impl Default for Opts {
106 fn default() -> Self {
107 Self {
108 coloured: false,
109 output: Output::default(),
110 level: LogLevel::default(),
111 report_caller: false,
112 timestamp_format: TSFormat::default(),
113 pad_level: false,
114 pad_amount: 5,
115 pad_side: PadSide::default(),
116 msg_separator: ": ".to_string(),
117 arrow_char: "▶".to_string(),
118 colors: Colors::default(),
119 }
120 }
121}
122
123impl Opts {
124 pub fn new() -> Opts {
134 Opts::default()
135 }
136
137 pub fn coloured(&self) -> bool {
139 self.coloured
140 }
141
142 pub fn output(&self) -> &Output {
144 &self.output
145 }
146
147 pub fn level(&self) -> LogLevel {
149 self.level
150 }
151
152 pub fn report_caller(&self) -> bool {
154 self.report_caller
155 }
156
157 pub fn timestamp_format(&self) -> &TSFormat {
159 &self.timestamp_format
160 }
161
162 pub fn pad_level(&self) -> bool {
164 self.pad_level
165 }
166
167 pub fn pad_amount(&self) -> usize {
169 self.pad_amount
170 }
171
172 pub fn pad_side(&self) -> PadSide {
174 self.pad_side
175 }
176
177 pub fn msg_separator(&self) -> &str {
179 &self.msg_separator
180 }
181
182 pub fn arrow_char(&self) -> &str {
184 &self.arrow_char
185 }
186
187 pub fn colors(&self) -> &Colors {
189 &self.colors
190 }
191
192 #[deprecated(since = "0.6.1", note = "Use timestamp_format() instead")]
194 pub fn time_format(&self) -> Option<&str> {
195 match &self.timestamp_format {
196 TSFormat::Custom(s) => Some(s.as_str()),
197 _ => Some(self.timestamp_format.to_format_string()),
198 }
199 }
200}
201
202#[derive(Clone, Debug)]
217pub struct OptsBuilder {
218 coloured: bool,
219 output: Output,
220 level: LogLevel,
221 report_caller: bool,
222 timestamp_format: TSFormat,
223 pad_level: bool,
224 pad_amount: usize,
225 pad_side: PadSide,
226 msg_separator: String,
227 arrow_char: String,
228 colors: Colors,
229}
230
231impl Default for OptsBuilder {
232 fn default() -> Self {
233 Self::new()
234 }
235}
236
237impl OptsBuilder {
238 pub fn new() -> Self {
240 Self {
241 coloured: false,
242 output: Output::default(),
243 level: LogLevel::default(),
244 report_caller: false,
245 timestamp_format: TSFormat::default(),
246 pad_level: false,
247 pad_amount: 5,
248 pad_side: PadSide::default(),
249 msg_separator: ": ".to_string(),
250 arrow_char: "▶".to_string(),
251 colors: Colors::default(),
252 }
253 }
254
255 pub fn with_level_padding() -> Self {
257 Self::new()
258 .pad_level(true)
259 .pad_amount(5)
260 .pad_side(PadSide::Right)
261 }
262
263 pub fn no_caller() -> Self {
265 Self::new().report_caller(false)
266 }
267
268 pub fn coloured(mut self, coloured: bool) -> Self {
270 self.coloured = coloured;
271 self
272 }
273
274 pub fn output(mut self, output: Output) -> Self {
276 self.output = output;
277 self
278 }
279
280 pub fn level(mut self, level: LogLevel) -> Self {
282 self.level = level;
283 self
284 }
285
286 pub fn report_caller(mut self, report: bool) -> Self {
288 self.report_caller = report;
289 self
290 }
291
292 pub fn timestamp_format(mut self, format: TSFormat) -> Self {
294 self.timestamp_format = format;
295 self
296 }
297
298 pub fn pad_level(mut self, pad: bool) -> Self {
300 self.pad_level = pad;
301 self
302 }
303
304 pub fn pad_amount(mut self, amount: usize) -> Self {
306 self.pad_amount = amount;
307 self
308 }
309
310 pub fn pad_side(mut self, side: PadSide) -> Self {
312 self.pad_side = side;
313 self
314 }
315
316 pub fn msg_separator(mut self, sep: impl Into<String>) -> Self {
318 self.msg_separator = sep.into();
319 self
320 }
321
322 pub fn arrow_char(mut self, arrow: impl Into<String>) -> Self {
324 self.arrow_char = arrow.into();
325 self
326 }
327
328 pub fn colors(mut self, colors: Colors) -> Self {
330 self.colors = colors;
331 self
332 }
333
334 #[deprecated(since = "0.6.1", note = "Use timestamp_format() instead")]
349 pub fn time_format(mut self, format: impl Into<String>) -> Self {
350 self.timestamp_format = TSFormat::Custom(format.into());
351 self
352 }
353
354 pub fn build(self) -> Result<Opts> {
360 if let TSFormat::Custom(ref fmt) = self.timestamp_format {
362 validate_time_format(fmt)?;
363 }
364
365 Ok(Opts {
366 coloured: self.coloured,
367 output: self.output,
368 level: self.level,
369 report_caller: self.report_caller,
370 timestamp_format: self.timestamp_format,
371 pad_level: self.pad_level,
372 pad_amount: self.pad_amount,
373 pad_side: self.pad_side,
374 msg_separator: self.msg_separator,
375 arrow_char: self.arrow_char,
376 colors: self.colors,
377 })
378 }
379}
380
381fn validate_time_format(format: &str) -> Result<()> {
383 match std::panic::catch_unwind(|| {
384 Local::now().format(format).to_string();
385 }) {
386 Ok(_) => Ok(()),
387 Err(_) => Err(TwygError::ConfigError(format!(
388 "invalid time format string: {}",
389 format
390 ))),
391 }
392}
393
394pub mod compat {
396 use super::*;
397 use crate::out;
398
399 #[deprecated(since = "0.6.0", note = "Use Output::default() instead")]
401 pub fn default_file() -> Option<String> {
402 Some(out::STDOUT.to_string())
403 }
404
405 #[deprecated(since = "0.6.0", note = "Use LogLevel::default() instead")]
407 pub fn default_level() -> Option<String> {
408 Some("error".to_string())
409 }
410
411 #[deprecated(since = "0.6.0", note = "Use Opts::new() or set time_format directly")]
413 pub fn default_ts_format() -> Option<String> {
414 Some(DEFAULT_TS_FORMAT.to_string())
415 }
416}
417
418#[allow(deprecated)]
420#[deprecated(since = "0.6.0", note = "Use Output::default() instead")]
421pub use compat::default_file;
422
423#[allow(deprecated)]
424#[deprecated(since = "0.6.0", note = "Use LogLevel::default() instead")]
425pub use compat::default_level;
426
427#[allow(deprecated)]
428#[deprecated(since = "0.6.0", note = "Use Opts::new() or set time_format directly")]
429pub use compat::default_ts_format;
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434
435 #[test]
436 fn test_default_opts() {
437 let opts = Opts::default();
438 assert!(!opts.coloured());
439 assert_eq!(opts.output(), &Output::Stdout);
440 assert_eq!(opts.level(), LogLevel::Error);
441 assert!(!opts.report_caller());
442 assert_eq!(opts.timestamp_format(), &TSFormat::Standard);
444 }
445
446 #[test]
447 fn test_new_opts_sets_defaults() {
448 let opts = Opts::new();
449 assert!(!opts.coloured());
450 assert_eq!(opts.output(), &Output::Stdout);
451 assert_eq!(opts.level(), LogLevel::Error);
452 assert!(!opts.report_caller());
453 assert_eq!(opts.timestamp_format(), &TSFormat::Standard);
454 }
455
456 #[test]
457 fn test_opts_clone() {
458 let opts1 = Opts::new();
459 let opts2 = opts1.clone();
460 assert_eq!(opts1.output(), opts2.output());
461 assert_eq!(opts1.level(), opts2.level());
462 assert_eq!(opts1.coloured(), opts2.coloured());
463 }
464
465 #[test]
466 fn test_opts_debug() {
467 let opts = Opts::new();
468 let debug_str = format!("{:?}", opts);
469 assert!(debug_str.contains("Opts"));
470 }
471
472 #[test]
473 fn test_opts_serialize_deserialize() {
474 let opts = OptsBuilder::new()
475 .coloured(true)
476 .output(Output::Stderr)
477 .level(LogLevel::Debug)
478 .report_caller(true)
479 .timestamp_format(TSFormat::TimeOnly)
480 .build()
481 .unwrap();
482
483 let serialized = serde_json::to_string(&opts).unwrap();
484 let deserialized: Opts = serde_json::from_str(&serialized).unwrap();
485
486 assert_eq!(opts.coloured(), deserialized.coloured());
487 assert_eq!(opts.output(), deserialized.output());
488 assert_eq!(opts.level(), deserialized.level());
489 assert_eq!(opts.report_caller(), deserialized.report_caller());
490 assert_eq!(opts.timestamp_format(), deserialized.timestamp_format());
491 }
492
493 #[test]
494 fn test_opts_builder_with_custom_values() {
495 let opts = OptsBuilder::new()
496 .coloured(true)
497 .output(Output::file("/tmp/test.log"))
498 .level(LogLevel::Trace)
499 .report_caller(true)
500 .timestamp_format(TSFormat::Custom("%Y-%m-%d".to_string()))
501 .build()
502 .unwrap();
503
504 assert!(opts.coloured());
505 assert_eq!(opts.output(), &Output::file("/tmp/test.log"));
506 assert_eq!(opts.level(), LogLevel::Trace);
507 assert!(opts.report_caller());
508 assert_eq!(
509 opts.timestamp_format(),
510 &TSFormat::Custom("%Y-%m-%d".to_string())
511 );
512 }
513
514 #[test]
515 fn test_opts_builder_with_different_outputs() {
516 let stdout_opts = OptsBuilder::new().output(Output::Stdout).build().unwrap();
517 assert_eq!(stdout_opts.output(), &Output::Stdout);
518
519 let stderr_opts = OptsBuilder::new().output(Output::Stderr).build().unwrap();
520 assert_eq!(stderr_opts.output(), &Output::Stderr);
521
522 let file_opts = OptsBuilder::new()
523 .output(Output::file("/var/log/app.log"))
524 .build()
525 .unwrap();
526 assert_eq!(file_opts.output(), &Output::file("/var/log/app.log"));
527 }
528
529 #[test]
530 fn test_opts_builder_default() {
531 let opts = OptsBuilder::default().build().unwrap();
532 assert!(!opts.coloured());
533 assert_eq!(opts.output(), &Output::Stdout);
534 assert_eq!(opts.level(), LogLevel::Error);
535 assert!(!opts.report_caller());
536 assert_eq!(opts.timestamp_format(), &TSFormat::Standard);
538 }
539
540 #[test]
541 fn test_validate_time_format_valid() {
542 assert!(validate_time_format("%Y-%m-%d %H:%M:%S").is_ok());
543 assert!(validate_time_format("%H:%M:%S").is_ok());
544 assert!(validate_time_format("%Y-%m-%d").is_ok());
545 }
546
547 #[test]
548 fn test_validate_time_format_invalid() {
549 let result = validate_time_format("%Z"); let _ = result;
554 }
555
556 #[test]
557 fn test_opts_builder_chaining() {
558 let opts = OptsBuilder::new()
559 .coloured(true)
560 .level(LogLevel::Debug)
561 .report_caller(true)
562 .output(Output::Stderr)
563 .build()
564 .unwrap();
565
566 assert!(opts.coloured());
567 assert_eq!(opts.level(), LogLevel::Debug);
568 assert!(opts.report_caller());
569 assert_eq!(opts.output(), &Output::Stderr);
570 }
571
572 #[test]
574 #[allow(deprecated)]
575 fn test_deprecated_default_file() {
576 let file = default_file();
577 assert_eq!(file, Some("stdout".to_string()));
578 }
579
580 #[test]
581 #[allow(deprecated)]
582 fn test_deprecated_default_level() {
583 let level = default_level();
584 assert_eq!(level, Some("error".to_string()));
585 }
586
587 #[test]
588 #[allow(deprecated)]
589 fn test_deprecated_default_ts_format() {
590 let format = default_ts_format();
591 assert_eq!(format, Some("%Y-%m-%d %H:%M:%S".to_string()));
592 }
593
594 #[test]
595 fn test_pad_side_default() {
596 assert_eq!(PadSide::default(), PadSide::Right);
597 }
598
599 #[test]
600 fn test_pad_side_eq() {
601 assert_eq!(PadSide::Left, PadSide::Left);
602 assert_eq!(PadSide::Right, PadSide::Right);
603 assert_ne!(PadSide::Left, PadSide::Right);
604 }
605
606 #[test]
607 fn test_pad_side_clone() {
608 let left = PadSide::Left;
609 let cloned = left.clone();
610 assert_eq!(left, cloned);
611 }
612
613 #[test]
614 fn test_pad_side_debug() {
615 let debug_str = format!("{:?}", PadSide::Left);
616 assert!(debug_str.contains("Left"));
617 }
618
619 #[test]
620 fn test_opts_all_getters() {
621 let colors = Colors::default();
622 let opts = OptsBuilder::new()
623 .coloured(true)
624 .output(Output::Stderr)
625 .level(LogLevel::Debug)
626 .report_caller(true)
627 .timestamp_format(TSFormat::TimeOnly)
628 .pad_level(true)
629 .pad_amount(7)
630 .pad_side(PadSide::Left)
631 .msg_separator(" | ")
632 .arrow_char("→")
633 .colors(colors.clone())
634 .build()
635 .unwrap();
636
637 assert!(opts.coloured());
638 assert_eq!(opts.output(), &Output::Stderr);
639 assert_eq!(opts.level(), LogLevel::Debug);
640 assert!(opts.report_caller());
641 assert_eq!(opts.timestamp_format(), &TSFormat::TimeOnly);
642 assert!(opts.pad_level());
643 assert_eq!(opts.pad_amount(), 7);
644 assert_eq!(opts.pad_side(), PadSide::Left);
645 assert_eq!(opts.msg_separator(), " | ");
646 assert_eq!(opts.arrow_char(), "→");
647 assert_eq!(opts.colors(), &colors);
648 }
649
650 #[test]
651 fn test_opts_builder_preset_with_level_padding() {
652 let opts = OptsBuilder::with_level_padding().build().unwrap();
653 assert!(opts.pad_level());
654 assert_eq!(opts.pad_amount(), 5);
655 assert_eq!(opts.pad_side(), PadSide::Right);
656 }
657
658 #[test]
659 fn test_opts_builder_preset_no_caller() {
660 let opts = OptsBuilder::no_caller().build().unwrap();
661 assert!(!opts.report_caller());
662 }
663
664 #[test]
665 fn test_opts_builder_chaining_all_methods() {
666 let opts = OptsBuilder::new()
667 .coloured(false)
668 .output(Output::file("/tmp/test.log"))
669 .level(LogLevel::Trace)
670 .report_caller(false)
671 .timestamp_format(TSFormat::RFC3339)
672 .pad_level(true)
673 .pad_amount(10)
674 .pad_side(PadSide::Left)
675 .msg_separator(" :: ")
676 .arrow_char("»")
677 .colors(Colors::default())
678 .build()
679 .unwrap();
680
681 assert!(!opts.coloured());
682 assert_eq!(opts.level(), LogLevel::Trace);
683 assert_eq!(opts.pad_amount(), 10);
684 assert_eq!(opts.msg_separator(), " :: ");
685 assert_eq!(opts.arrow_char(), "»");
686 }
687
688 #[test]
689 #[allow(deprecated)]
690 fn test_opts_deprecated_time_format_method() {
691 let opts = OptsBuilder::new().time_format("%H:%M").build().unwrap();
692
693 assert!(opts.time_format().is_some());
695 let format = opts.time_format().unwrap();
696 assert_eq!(format, "%H:%M");
697 }
698
699 #[test]
700 #[allow(deprecated)]
701 fn test_opts_builder_deprecated_time_format() {
702 let opts = OptsBuilder::new().time_format("%Y%m%d").build().unwrap();
703
704 match opts.timestamp_format() {
706 TSFormat::Custom(s) => assert_eq!(s, "%Y%m%d"),
707 _ => panic!("Expected Custom variant"),
708 }
709 }
710
711 #[test]
712 fn test_validate_time_format_various_formats() {
713 assert!(validate_time_format("%Y").is_ok());
715 assert!(validate_time_format("%m").is_ok());
716 assert!(validate_time_format("%d").is_ok());
717 assert!(validate_time_format("%H").is_ok());
718 assert!(validate_time_format("%M").is_ok());
719 assert!(validate_time_format("%S").is_ok());
720 assert!(validate_time_format("%Y-%m-%d %H:%M:%S").is_ok());
721 assert!(validate_time_format("%Y%m%d.%H%M%S").is_ok());
722 }
723
724 #[test]
725 fn test_opts_serialize_with_all_fields() {
726 let opts = OptsBuilder::new()
727 .coloured(true)
728 .output(Output::Stderr)
729 .level(LogLevel::Warn)
730 .report_caller(true)
731 .timestamp_format(TSFormat::Simple)
732 .pad_level(true)
733 .pad_amount(8)
734 .pad_side(PadSide::Left)
735 .msg_separator(" => ")
736 .arrow_char("⇒")
737 .colors(Colors::default())
738 .build()
739 .unwrap();
740
741 let serialized = serde_json::to_string(&opts).unwrap();
742 let deserialized: Opts = serde_json::from_str(&serialized).unwrap();
743
744 assert_eq!(opts.coloured(), deserialized.coloured());
745 assert_eq!(opts.output(), deserialized.output());
746 assert_eq!(opts.level(), deserialized.level());
747 assert_eq!(opts.report_caller(), deserialized.report_caller());
748 assert_eq!(opts.pad_level(), deserialized.pad_level());
749 assert_eq!(opts.pad_amount(), deserialized.pad_amount());
750 assert_eq!(opts.pad_side(), deserialized.pad_side());
751 assert_eq!(opts.msg_separator(), deserialized.msg_separator());
752 assert_eq!(opts.arrow_char(), deserialized.arrow_char());
753 }
754
755 #[test]
756 fn test_pad_side_serialize_deserialize() {
757 let left = PadSide::Left;
758 let serialized = serde_json::to_string(&left).unwrap();
759 let deserialized: PadSide = serde_json::from_str(&serialized).unwrap();
760 assert_eq!(left, deserialized);
761
762 let right = PadSide::Right;
763 let serialized = serde_json::to_string(&right).unwrap();
764 let deserialized: PadSide = serde_json::from_str(&serialized).unwrap();
765 assert_eq!(right, deserialized);
766 }
767
768 #[test]
769 fn test_opts_default_values_match_new() {
770 let default_opts = Opts::default();
771 let new_opts = Opts::new();
772
773 assert_eq!(default_opts.coloured(), new_opts.coloured());
774 assert_eq!(default_opts.output(), new_opts.output());
775 assert_eq!(default_opts.level(), new_opts.level());
776 assert_eq!(default_opts.report_caller(), new_opts.report_caller());
777 assert_eq!(default_opts.pad_level(), new_opts.pad_level());
778 assert_eq!(default_opts.pad_amount(), new_opts.pad_amount());
779 assert_eq!(default_opts.pad_side(), new_opts.pad_side());
780 }
781
782 #[test]
783 fn test_opts_builder_multiple_builds() {
784 let builder = OptsBuilder::new().level(LogLevel::Debug).coloured(true);
785
786 let opts1 = builder.clone().build().unwrap();
788 let opts2 = builder.clone().build().unwrap();
789
790 assert_eq!(opts1.level(), opts2.level());
791 assert_eq!(opts1.coloured(), opts2.coloured());
792 }
793
794 #[test]
795 fn test_default_helper_functions() {
796 assert_eq!(default_pad_amount(), 5);
797 assert_eq!(default_msg_separator(), ": ");
798 assert_eq!(default_arrow_char(), "▶");
799 }
800
801 #[test]
802 fn test_opts_deserialize_partial_toml_uses_defaults() {
803 let toml_str = r#"level = "debug""#;
804 let opts: Opts = toml::from_str(toml_str).unwrap();
805
806 assert_eq!(opts.level(), LogLevel::Debug);
808
809 assert!(!opts.coloured());
811 assert_eq!(opts.output(), &Output::Stdout);
812 assert!(!opts.report_caller());
813 assert_eq!(opts.timestamp_format(), &TSFormat::Standard);
814 assert!(!opts.pad_level());
815 assert_eq!(opts.pad_amount(), 5);
816 assert_eq!(opts.pad_side(), PadSide::Right);
817 assert_eq!(opts.msg_separator(), ": ");
818 assert_eq!(opts.arrow_char(), "▶");
819 }
820}