1#[cfg(all(feature = "log", not(feature = "tracing")))]
2use log::{debug, error, info, trace, warn};
3use std::{
4 fmt::Display,
5 ops::{Deref, DerefMut},
6 path::{Path, PathBuf},
7};
8
9use super::metadata::{ErrorMetadata, MetadataValue};
10#[derive(Debug, Clone, PartialEq, Default)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum OperationResult {
13 Suc,
14 #[default]
15 Fail,
16 Cancel,
17}
18
19const DEFAULT_MOD_PATH: &str = module_path!();
21
22#[macro_export]
24macro_rules! op_context {
25 ($target:expr) => {
26 $crate::OperationContext::doing($target).with_mod_path(module_path!())
27 };
28}
29
30#[derive(Debug, Clone, PartialEq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct OperationContext {
33 context: CallContext,
34 result: OperationResult,
35 exit_log: bool,
36 mod_path: String,
37 #[cfg_attr(feature = "serde", serde(default))]
38 action: Option<String>,
39 #[cfg_attr(feature = "serde", serde(default))]
40 locator: Option<String>,
41 target: Option<String>,
42 #[cfg_attr(feature = "serde", serde(default))]
43 path: Vec<String>,
44 #[cfg_attr(feature = "serde", serde(default))]
45 #[cfg_attr(
46 feature = "serde",
47 serde(skip_serializing_if = "ErrorMetadata::is_empty")
48 )]
49 metadata: ErrorMetadata,
50}
51impl Default for OperationContext {
52 fn default() -> Self {
53 Self {
54 context: CallContext::default(),
55 action: None,
56 locator: None,
57 target: None,
58 path: Vec::new(),
59 result: OperationResult::Fail,
60 exit_log: false,
61 mod_path: DEFAULT_MOD_PATH.into(),
62 metadata: ErrorMetadata::default(),
63 }
64 }
65}
66pub type WithContext = OperationContext;
67impl From<CallContext> for OperationContext {
68 fn from(value: CallContext) -> Self {
69 OperationContext {
70 context: value,
71 result: OperationResult::Fail,
72 action: None,
73 locator: None,
74 target: None,
75 path: Vec::new(),
76 exit_log: false,
77 mod_path: DEFAULT_MOD_PATH.into(),
78 metadata: ErrorMetadata::default(),
79 }
80 }
81}
82
83impl Drop for OperationContext {
84 fn drop(&mut self) {
85 if !self.exit_log {
86 return;
87 }
88
89 #[cfg(feature = "tracing")]
90 {
91 let ctx = self.format_context();
92 match self.result() {
93 OperationResult::Suc => {
94 tracing::info!(
95 target: "domain",
96 mod_path = %self.mod_path,
97 "suc! {ctx}"
98 )
99 }
100 OperationResult::Fail => {
101 tracing::error!(
102 target: "domain",
103 mod_path = %self.mod_path,
104 "fail! {ctx}"
105 )
106 }
107 OperationResult::Cancel => {
108 tracing::warn!(
109 target: "domain",
110 mod_path = %self.mod_path,
111 "cancel! {ctx}"
112 )
113 }
114 }
115 }
116
117 #[cfg(all(feature = "log", not(feature = "tracing")))]
118 {
119 match self.result() {
120 OperationResult::Suc => {
121 info!(target: self.mod_path.as_str(), "suc! {}", self.format_context());
122 }
123 OperationResult::Fail => {
124 error!(target: self.mod_path.as_str(), "fail! {}", self.format_context());
125 }
126 OperationResult::Cancel => {
127 warn!(target: self.mod_path.as_str(), "cancel! {}", self.format_context());
128 }
129 }
130 }
131 }
132}
133
134impl Display for OperationContext {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 if let Some(action) = &self.action {
137 writeln!(f, "doing: {action}")?;
138 }
139 if let Some(locator) = &self.locator {
140 writeln!(f, "at: {locator}")?;
141 }
142 if let Some(target) = &self.target {
143 if self.action.as_deref() != Some(target.as_str()) {
144 writeln!(f, "want: {target}")?;
145 }
146 }
147 if let Some(path) = self.normalized_path_string() {
148 if self.target.as_deref() != Some(path.as_str()) {
149 writeln!(f, "path: {path}")?;
150 }
151 }
152 for (i, (k, v)) in self.context().items.iter().enumerate() {
153 writeln!(f, "{}. {k}: {v} ", i + 1)?;
154 }
155 Ok(())
156 }
157}
158pub trait ContextRecord<S1, S2> {
167 fn record_field(&mut self, key: S1, val: S2);
168
169 fn record(&mut self, key: S1, val: S2)
170 where
171 Self: Sized,
172 {
173 self.record_field(key, val);
174 }
175}
176
177impl<S1> ContextRecord<S1, String> for OperationContext
178where
179 S1: Into<String>,
180{
181 fn record_field(&mut self, key: S1, val: String) {
182 self.context.items.push((key.into(), val));
183 }
184}
185
186impl<S1> ContextRecord<S1, &str> for OperationContext
187where
188 S1: Into<String>,
189{
190 fn record_field(&mut self, key: S1, val: &str) {
191 self.context.items.push((key.into(), val.into()));
192 }
193}
194
195impl<S1> ContextRecord<S1, &PathBuf> for OperationContext
198where
199 S1: Into<String>,
200{
201 fn record_field(&mut self, key: S1, val: &PathBuf) {
202 self.context
203 .items
204 .push((key.into(), format!("{}", val.display())));
205 }
206}
207impl<S1> ContextRecord<S1, &Path> for OperationContext
208where
209 S1: Into<String>,
210{
211 fn record_field(&mut self, key: S1, val: &Path) {
212 self.context
213 .items
214 .push((key.into(), format!("{}", val.display())));
215 }
216}
217
218impl OperationContext {
219 pub(crate) fn from_target(target: String) -> Self {
220 Self {
221 action: None,
222 locator: None,
223 target: Some(target.clone()),
224 path: vec![target],
225 context: CallContext::default(),
226 result: OperationResult::Fail,
227 exit_log: false,
228 mod_path: DEFAULT_MOD_PATH.into(),
229 metadata: ErrorMetadata::default(),
230 }
231 }
232
233 pub(crate) fn push_target_segment(&mut self, target: String) {
234 if target.is_empty() {
235 return;
236 }
237
238 let locator_at_end = self
239 .locator
240 .as_ref()
241 .is_some_and(|locator| self.path.last() == Some(locator));
242
243 if self.target.is_none() {
244 self.target = Some(target.clone());
245 if self.path.is_empty() {
246 self.path.push(target);
247 return;
248 }
249 if self.path.first() != Some(&target) {
250 self.path.insert(0, target);
251 }
252 } else {
253 let root = self.target.clone().expect("checked above");
254 if self.path.is_empty() {
255 self.path.push(root.clone());
256 }
257 if self.path.first() != Some(&root) {
258 self.path.insert(0, root);
259 }
260
261 let insert_index = if locator_at_end {
262 self.path.len().saturating_sub(1)
263 } else {
264 self.path.len()
265 };
266 let prev = insert_index
267 .checked_sub(1)
268 .and_then(|idx| self.path.get(idx));
269 let current = self.path.get(insert_index);
270 if prev != Some(&target) && current != Some(&target) {
271 self.path.insert(insert_index, target);
272 }
273 }
274 }
275
276 pub fn context(&self) -> &CallContext {
277 &self.context
278 }
279
280 pub fn result(&self) -> &OperationResult {
281 &self.result
282 }
283
284 pub fn exit_log(&self) -> &bool {
285 &self.exit_log
286 }
287
288 pub fn mod_path(&self) -> &String {
289 &self.mod_path
290 }
291
292 pub fn target(&self) -> &Option<String> {
293 &self.target
294 }
295
296 pub fn action(&self) -> &Option<String> {
297 &self.action
298 }
299
300 pub fn locator(&self) -> &Option<String> {
301 &self.locator
302 }
303
304 pub fn path(&self) -> &[String] {
305 &self.path
306 }
307
308 pub fn metadata(&self) -> &ErrorMetadata {
309 &self.metadata
310 }
311
312 pub fn new() -> Self {
313 Self {
314 target: None,
315 action: None,
316 locator: None,
317 path: Vec::new(),
318 context: CallContext::default(),
319 result: OperationResult::Fail,
320 exit_log: false,
321 mod_path: DEFAULT_MOD_PATH.into(),
322 metadata: ErrorMetadata::default(),
323 }
324 }
325 #[deprecated(
326 since = "0.7.0",
327 note = "use doing(...) for action contexts; use at(...) for locator/resource contexts"
328 )]
329 pub fn want<S: Into<String>>(target: S) -> Self {
330 Self::from_target(target.into())
331 }
332 pub fn doing<S: Into<String>>(action: S) -> Self {
333 let action = action.into();
334 let mut ctx = Self::from_target(action.clone());
335 ctx.action = Some(action);
336 ctx
337 }
338 pub fn at<S: Into<String>>(locator: S) -> Self {
339 let locator = locator.into();
340 Self {
341 action: None,
342 locator: Some(locator.clone()),
343 target: None,
344 path: vec![locator],
345 context: CallContext::default(),
346 result: OperationResult::Fail,
347 exit_log: false,
348 mod_path: DEFAULT_MOD_PATH.into(),
349 metadata: ErrorMetadata::default(),
350 }
351 }
352 #[deprecated(since = "0.5.4", note = "use with_auto_log")]
353 pub fn with_exit_log(mut self) -> Self {
354 self.exit_log = true;
355 self
356 }
357 pub fn with_auto_log(mut self) -> Self {
358 self.exit_log = true;
359 self
360 }
361 pub fn with_mod_path<S: Into<String>>(mut self, path: S) -> Self {
362 self.mod_path = path.into();
363 self
364 }
365 #[deprecated(since = "0.5.4", note = "use record")]
366 pub fn with<S1: Into<String>, S2: Into<String>>(&mut self, key: S1, val: S2) {
367 self.context.items.push((key.into(), val.into()));
368 }
369
370 #[deprecated(since = "0.5.4", note = "use record")]
371 pub fn with_path<S1: Into<String>, S2: Into<PathBuf>>(&mut self, key: S1, val: S2) {
372 self.context
373 .items
374 .push((key.into(), format!("{}", val.into().display())));
375 }
376
377 #[deprecated(
378 since = "0.7.0",
379 note = "use with_doing(...) for action path segments; use with_at(...) for locator segments"
380 )]
381 pub fn with_want<S: Into<String>>(&mut self, target: S) {
382 self.push_target_segment(target.into());
383 }
384 pub fn with_doing<S: Into<String>>(&mut self, action: S) {
385 let action = action.into();
386 if action.is_empty() {
387 return;
388 }
389 if self.action.is_none() {
390 self.action = Some(action.clone());
391 }
392 self.push_target_segment(action)
393 }
394 pub fn with_at<S: Into<String>>(&mut self, locator: S) {
395 let locator = locator.into();
396 if locator.is_empty() {
397 return;
398 }
399 self.locator = Some(locator.clone());
400 if self.path.last() != Some(&locator) {
401 self.path.push(locator);
402 }
403 }
404
405 pub(crate) fn path_string_with_segments(&self, path: &[String]) -> Option<String> {
406 if path.is_empty() {
407 None
408 } else {
409 Some(path.join(" / "))
410 }
411 }
412
413 pub(crate) fn normalized_path_segments(&self) -> Vec<String> {
414 let mut path = Vec::new();
415
416 match (&self.action, &self.target) {
417 (Some(action), Some(target)) => {
418 path.push(target.clone());
419 for segment in self.path.iter().skip(1) {
420 if path.last() != Some(segment) {
421 path.push(segment.clone());
422 }
423 }
424 if path.last() != Some(action) && self.path.len() == 1 {
425 path.push(action.clone());
426 }
427 }
428 (Some(action), None) => {
429 path.push(action.clone());
430 for segment in &self.path {
431 if path.last() != Some(segment) {
432 path.push(segment.clone());
433 }
434 }
435 }
436 _ => {
437 for segment in &self.path {
438 if path.last() != Some(segment) {
439 path.push(segment.clone());
440 }
441 }
442 }
443 }
444
445 if let Some(locator) = &self.locator {
446 if path.last() != Some(locator) {
447 path.push(locator.clone());
448 }
449 }
450
451 path
452 }
453
454 pub(crate) fn normalized_path_string(&self) -> Option<String> {
455 self.path_string_with_segments(&self.normalized_path_segments())
456 }
457
458 pub(crate) fn into_at_context(mut self) -> Self {
459 if self.locator.is_none() && self.target.is_none() {
460 if self.path.len() == 1 {
461 self.locator = self.path.first().cloned();
462 } else if self.context.items.len() == 1 {
463 let (key, value) = &self.context.items[0];
464 if key == "key" || key == "path" {
465 self.locator = Some(value.clone());
466 }
467 }
468 }
469
470 if let Some(locator) = &self.locator {
471 if self.path.last() != Some(locator) {
472 self.path.push(locator.clone());
473 }
474 }
475
476 self
477 }
478 pub fn set_target<S: Into<String>>(&mut self, target: S) {
480 self.push_target_segment(target.into())
481 }
482
483 pub fn path_string(&self) -> Option<String> {
484 self.normalized_path_string()
485 }
486
487 pub fn record_field<S1, S2>(&mut self, key: S1, val: S2)
492 where
493 Self: ContextRecord<S1, S2>,
494 {
495 <Self as ContextRecord<S1, S2>>::record_field(self, key, val);
496 }
497
498 pub fn with_field<S1, S2>(mut self, key: S1, val: S2) -> Self
500 where
501 Self: ContextRecord<S1, S2>,
502 {
503 self.record_field(key, val);
504 self
505 }
506
507 pub fn record_meta<K, V>(&mut self, key: K, value: V)
512 where
513 K: Into<String>,
514 V: Into<MetadataValue>,
515 {
516 self.metadata.insert(key, value);
517 }
518
519 pub fn with_meta<K, V>(mut self, key: K, value: V) -> Self
521 where
522 K: Into<String>,
523 V: Into<MetadataValue>,
524 {
525 self.record_meta(key, value);
526 self
527 }
528
529 pub fn mark_suc(&mut self) {
530 self.result = OperationResult::Suc;
531 }
532 pub fn mark_cancel(&mut self) {
533 self.result = OperationResult::Cancel;
534 }
535
536 #[cfg_attr(not(any(feature = "log", feature = "tracing")), allow(dead_code))]
538 fn format_context(&self) -> String {
539 let want = self.target.clone().unwrap_or_default();
540 let path = self.normalized_path_string().unwrap_or_default();
541 let action = self.action.clone().unwrap_or_default();
542 let locator = self.locator.clone().unwrap_or_default();
543 let mut parts = Vec::new();
544 if !action.is_empty() {
545 parts.push(format!("doing={action}"));
546 } else if !want.is_empty() {
547 parts.push(format!("want={want}"));
548 }
549 if !locator.is_empty() {
550 parts.push(format!("at={locator}"));
551 }
552 if !path.is_empty() && path != want && path != locator {
553 parts.push(format!("path={path}"));
554 }
555 let head = if parts.is_empty() {
556 match (want.is_empty(), path.is_empty() || path == want) {
557 (true, true) => String::new(),
558 (false, true) => format!("want={want}"),
559 (false, false) => format!("want={want} path={path}"),
560 (true, false) => format!("path={path}"),
561 }
562 } else {
563 parts.join(" ")
564 };
565 if self.context.items.is_empty() {
566 return head;
567 }
568 if head.is_empty() {
569 let body = self.context.to_string();
570 body.strip_prefix('\n').unwrap_or(&body).to_string()
571 } else {
572 format!("{head}: {}", self.context)
573 }
574 }
575
576 pub fn scope(&mut self) -> OperationScope<'_> {
578 OperationScope {
579 ctx: self,
580 mark_success: false,
581 }
582 }
583
584 pub fn scoped_success(&mut self) -> OperationScope<'_> {
586 OperationScope {
587 ctx: self,
588 mark_success: true,
589 }
590 }
591
592 #[cfg(feature = "tracing")]
595 pub fn info<S: AsRef<str>>(&self, message: S) {
596 tracing::info!(
597 target: "domain",
598 mod_path = %self.mod_path,
599 "{}: {}",
600 self.format_context(),
601 message.as_ref()
602 );
603 }
604 #[cfg(all(feature = "log", not(feature = "tracing")))]
605 pub fn info<S: AsRef<str>>(&self, message: S) {
606 info!(target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
607 }
608 #[cfg(not(any(feature = "log", feature = "tracing")))]
609 pub fn info<S: AsRef<str>>(&self, _message: S) {}
610
611 #[cfg(feature = "tracing")]
612 pub fn debug<S: AsRef<str>>(&self, message: S) {
613 tracing::debug!(
614 target: "domain",
615 mod_path = %self.mod_path,
616 "{}: {}",
617 self.format_context(),
618 message.as_ref()
619 );
620 }
621 #[cfg(all(feature = "log", not(feature = "tracing")))]
622 pub fn debug<S: AsRef<str>>(&self, message: S) {
623 debug!( target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
624 }
625 #[cfg(not(any(feature = "log", feature = "tracing")))]
626 pub fn debug<S: AsRef<str>>(&self, _message: S) {}
627
628 #[cfg(feature = "tracing")]
629 pub fn warn<S: AsRef<str>>(&self, message: S) {
630 tracing::warn!(
631 target: "domain",
632 mod_path = %self.mod_path,
633 "{}: {}",
634 self.format_context(),
635 message.as_ref()
636 );
637 }
638 #[cfg(all(feature = "log", not(feature = "tracing")))]
639 pub fn warn<S: AsRef<str>>(&self, message: S) {
640 warn!( target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
641 }
642 #[cfg(not(any(feature = "log", feature = "tracing")))]
643 pub fn warn<S: AsRef<str>>(&self, _message: S) {}
644
645 #[cfg(feature = "tracing")]
646 pub fn error<S: AsRef<str>>(&self, message: S) {
647 tracing::error!(
648 target: "domain",
649 mod_path = %self.mod_path,
650 "{}: {}",
651 self.format_context(),
652 message.as_ref()
653 );
654 }
655 #[cfg(all(feature = "log", not(feature = "tracing")))]
656 pub fn error<S: AsRef<str>>(&self, message: S) {
657 error!(target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
658 }
659 #[cfg(not(any(feature = "log", feature = "tracing")))]
660 pub fn error<S: AsRef<str>>(&self, _message: S) {}
661
662 #[cfg(feature = "tracing")]
663 pub fn trace<S: AsRef<str>>(&self, message: S) {
664 tracing::trace!(
665 target: "domain",
666 mod_path = %self.mod_path,
667 "{}: {}",
668 self.format_context(),
669 message.as_ref()
670 );
671 }
672 #[cfg(all(feature = "log", not(feature = "tracing")))]
673 pub fn trace<S: AsRef<str>>(&self, message: S) {
674 trace!( target: self.mod_path.as_str(), "{}: {}", self.format_context(), message.as_ref());
675 }
676 #[cfg(not(any(feature = "log", feature = "tracing")))]
677 pub fn trace<S: AsRef<str>>(&self, _message: S) {}
678
679 pub fn log_info<S: AsRef<str>>(&self, message: S) {
681 self.info(message)
682 }
683 pub fn log_debug<S: AsRef<str>>(&self, message: S) {
684 self.debug(message)
685 }
686 pub fn log_warn<S: AsRef<str>>(&self, message: S) {
687 self.warn(message)
688 }
689 pub fn log_error<S: AsRef<str>>(&self, message: S) {
690 self.error(message)
691 }
692 pub fn log_trace<S: AsRef<str>>(&self, message: S) {
693 self.trace(message)
694 }
695
696 pub(crate) fn context_mut_for_report(&mut self) -> &mut CallContext {
697 &mut self.context
698 }
699
700 pub(crate) fn replace_target_for_report(&mut self, target: Option<String>) {
701 self.target = target;
702 }
703
704 pub(crate) fn replace_action_for_report(&mut self, action: Option<String>) {
705 self.action = action;
706 }
707
708 pub(crate) fn replace_locator_for_report(&mut self, locator: Option<String>) {
709 self.locator = locator;
710 }
711
712 pub(crate) fn replace_path_for_report(&mut self, path: Vec<String>) {
713 self.path = path;
714 }
715
716 pub(crate) fn replace_metadata_for_report(&mut self, metadata: ErrorMetadata) {
717 self.metadata = metadata;
718 }
719}
720
721pub struct OperationScope<'a> {
722 ctx: &'a mut OperationContext,
723 mark_success: bool,
724}
725
726impl<'a> OperationScope<'a> {
727 pub fn mark_success(&mut self) {
729 self.mark_success = true;
730 }
731
732 pub fn mark_failure(&mut self) {
734 self.mark_success = false;
735 }
736
737 pub fn cancel(&mut self) {
739 self.ctx.mark_cancel();
740 self.mark_success = false;
741 }
742}
743
744impl<'a> Deref for OperationScope<'a> {
745 type Target = OperationContext;
746
747 fn deref(&self) -> &Self::Target {
748 self.ctx
749 }
750}
751
752impl<'a> DerefMut for OperationScope<'a> {
753 fn deref_mut(&mut self) -> &mut Self::Target {
754 self.ctx
755 }
756}
757
758impl Drop for OperationScope<'_> {
759 fn drop(&mut self) {
760 if self.mark_success {
761 self.ctx.mark_suc();
762 }
763 }
764}
765
766impl From<String> for OperationContext {
767 fn from(value: String) -> Self {
768 Self {
769 target: None,
770 action: None,
771 locator: None,
772 path: Vec::new(),
773 context: CallContext::from(("key", value.to_string())),
774 result: OperationResult::Fail,
775 exit_log: false,
776 mod_path: DEFAULT_MOD_PATH.into(),
777 metadata: ErrorMetadata::default(),
778 }
779 }
780}
781
782impl From<&PathBuf> for OperationContext {
783 fn from(value: &PathBuf) -> Self {
784 Self {
785 target: None,
786 action: None,
787 locator: Some(format!("{}", value.display())),
788 path: Vec::new(),
789 context: CallContext::from(("path", format!("{}", value.display()))),
790 result: OperationResult::Fail,
791 exit_log: false,
792 mod_path: DEFAULT_MOD_PATH.into(),
793 metadata: ErrorMetadata::default(),
794 }
795 }
796}
797
798impl From<&Path> for OperationContext {
799 fn from(value: &Path) -> Self {
800 Self {
801 target: None,
802 action: None,
803 locator: Some(format!("{}", value.display())),
804 path: Vec::new(),
805 context: CallContext::from(("path", format!("{}", value.display()))),
806 result: OperationResult::Fail,
807 exit_log: false,
808 mod_path: DEFAULT_MOD_PATH.into(),
809 metadata: ErrorMetadata::default(),
810 }
811 }
812}
813
814impl From<&str> for OperationContext {
815 fn from(value: &str) -> Self {
816 Self {
817 target: None,
818 action: None,
819 locator: None,
820 path: Vec::new(),
821 context: CallContext::from(("key", value.to_string())),
822 result: OperationResult::Fail,
823 exit_log: false,
824 mod_path: DEFAULT_MOD_PATH.into(),
825 metadata: ErrorMetadata::default(),
826 }
827 }
828}
829
830impl From<(&str, &str)> for OperationContext {
831 fn from(value: (&str, &str)) -> Self {
832 Self {
833 target: None,
834 action: None,
835 locator: None,
836 path: Vec::new(),
837 context: CallContext::from((value.0, value.1)),
838 result: OperationResult::Fail,
839 exit_log: false,
840 mod_path: DEFAULT_MOD_PATH.into(),
841 metadata: ErrorMetadata::default(),
842 }
843 }
844}
845
846impl From<(&str, String)> for OperationContext {
847 fn from(value: (&str, String)) -> Self {
848 Self {
849 target: None,
850 action: None,
851 locator: None,
852 path: Vec::new(),
853 context: CallContext::from((value.0, value.1)),
854 result: OperationResult::Fail,
855 exit_log: false,
856 mod_path: DEFAULT_MOD_PATH.into(),
857 metadata: ErrorMetadata::default(),
858 }
859 }
860}
861trait NotAsRefStr: AsRef<Path> {}
863
864impl NotAsRefStr for PathBuf {}
866impl NotAsRefStr for Path {}
867impl<T: AsRef<Path> + ?Sized> NotAsRefStr for &T where T: NotAsRefStr {}
868
869impl<V: AsRef<Path>> From<(&str, V)> for OperationContext
870where
871 V: NotAsRefStr,
872{
873 fn from(value: (&str, V)) -> Self {
874 Self {
875 target: None,
876 action: None,
877 locator: None,
878 path: Vec::new(),
879 context: CallContext {
880 items: vec![(
881 value.0.to_string(),
882 format!("{}", value.1.as_ref().display()),
883 )],
884 },
885 result: OperationResult::Fail,
886 exit_log: false,
887 mod_path: DEFAULT_MOD_PATH.into(),
888 metadata: ErrorMetadata::default(),
889 }
890 }
891}
892
893impl From<(String, String)> for OperationContext {
894 fn from(value: (String, String)) -> Self {
895 Self {
896 target: None,
897 action: None,
898 locator: None,
899 path: Vec::new(),
900 context: CallContext::from((value.0, value.1)),
901 result: OperationResult::Fail,
902 exit_log: false,
903 mod_path: DEFAULT_MOD_PATH.into(),
904 metadata: ErrorMetadata::default(),
905 }
906 }
907}
908
909impl From<&OperationContext> for OperationContext {
910 fn from(value: &OperationContext) -> Self {
911 value.clone()
912 }
913}
914
915#[derive(Default, Debug, Clone, PartialEq)]
916#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
917pub struct CallContext {
918 pub items: Vec<(String, String)>,
919}
920
921impl<K: AsRef<str>, V: AsRef<str>> From<(K, V)> for CallContext {
922 fn from(value: (K, V)) -> Self {
923 Self {
924 items: vec![(value.0.as_ref().to_string(), value.1.as_ref().to_string())],
925 }
926 }
927}
928
929pub trait ContextAdd<T> {
930 fn add_context(&mut self, val: T);
931}
932
933impl<K: Into<String>> ContextAdd<(K, String)> for OperationContext {
934 fn add_context(&mut self, val: (K, String)) {
935 self.record_field(val.0.into(), val.1);
936 }
937}
938impl<K: Into<String>> ContextAdd<(K, &String)> for OperationContext {
939 fn add_context(&mut self, val: (K, &String)) {
940 self.record_field(val.0.into(), val.1.clone());
941 }
942}
943impl<K: Into<String>> ContextAdd<(K, &str)> for OperationContext {
944 fn add_context(&mut self, val: (K, &str)) {
945 self.record_field(val.0.into(), val.1.to_string());
946 }
947}
948
949impl<K: Into<String>> ContextAdd<(K, &PathBuf)> for OperationContext {
950 fn add_context(&mut self, val: (K, &PathBuf)) {
951 self.record_field(val.0.into(), format!("{}", val.1.display()));
952 }
953}
954impl<K: Into<String>> ContextAdd<(K, &Path)> for OperationContext {
955 fn add_context(&mut self, val: (K, &Path)) {
956 self.record_field(val.0.into(), format!("{}", val.1.display()));
957 }
958}
959
960impl Display for CallContext {
961 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
962 if !self.items.is_empty() {
963 writeln!(f, "\ncall context:")?;
964 }
965 for (k, v) in &self.items {
966 writeln!(f, "\t{k} : {v}")?;
967 }
968 Ok(())
969 }
970}
971
972#[cfg(test)]
973mod tests {
974 use super::*;
975 use std::path::PathBuf;
976
977 #[test]
978 fn test_op_context_macro_sets_callsite_mod_path() {
979 let ctx = crate::op_context!("macro_target");
980 assert_eq!(*ctx.target(), Some("macro_target".to_string()));
981 assert_eq!(ctx.mod_path().as_str(), module_path!());
982 }
983
984 #[test]
985 fn test_withcontext_new() {
986 let ctx = OperationContext::new();
987 assert!(ctx.target.is_none());
988 assert_eq!(ctx.context().items.len(), 0);
989 }
990
991 #[test]
992 #[allow(deprecated)]
993 fn test_withcontext_want() {
994 let ctx = OperationContext::want("test_target");
995 assert_eq!(*ctx.target(), Some("test_target".to_string()));
996 assert_eq!(ctx.path(), &["test_target".to_string()]);
997 assert_eq!(ctx.context().items.len(), 0);
998 }
999
1000 #[test]
1001 fn test_doing_records_action_with_compat_target_projection() {
1002 let mut ctx = OperationContext::doing("load_config");
1003 ctx.with_at("config.toml");
1004
1005 assert_eq!(ctx.action().as_deref(), Some("load_config"));
1006 assert_eq!(ctx.locator().as_deref(), Some("config.toml"));
1007 assert_eq!(ctx.target().as_deref(), Some("load_config"));
1008 assert_eq!(
1009 ctx.path(),
1010 &["load_config".to_string(), "config.toml".to_string()]
1011 );
1012
1013 let rendered = ctx.to_string();
1014 assert!(rendered.contains("doing: load_config"));
1015 assert!(rendered.contains("at: config.toml"));
1016 }
1017
1018 #[test]
1019 fn test_withcontext_with() {
1020 let mut ctx = OperationContext::new();
1021 ctx.record("key1", "value1");
1022 ctx.record("key2", "value2");
1023
1024 assert_eq!(ctx.context().items.len(), 2);
1025 assert_eq!(
1026 ctx.context().items[0],
1027 ("key1".to_string(), "value1".to_string())
1028 );
1029 assert_eq!(
1030 ctx.context().items[1],
1031 ("key2".to_string(), "value2".to_string())
1032 );
1033 }
1034
1035 #[test]
1036 fn test_withcontext_with_path() {
1037 let mut ctx = OperationContext::new();
1038 let path = PathBuf::from("/test/path");
1039 ctx.record("file_path", &path);
1040
1041 assert_eq!(ctx.context().items.len(), 1);
1042 assert!(ctx.context().items[0].1.contains("/test/path"));
1043 }
1044
1045 #[test]
1046 #[allow(deprecated)]
1047 fn test_withcontext_with_want() {
1048 let mut ctx = OperationContext::new();
1049 ctx.with_want("new_target");
1050
1051 assert_eq!(*ctx.target(), Some("new_target".to_string()));
1052 assert_eq!(ctx.path(), &["new_target".to_string()]);
1053 }
1054
1055 #[test]
1056 #[allow(deprecated)]
1057 fn test_withcontext_with_want_appends_path() {
1058 let mut ctx = OperationContext::want("place_order");
1059 ctx.with_want("read_order_payload");
1060 ctx.with_want("parse_order");
1061
1062 assert_eq!(*ctx.target(), Some("place_order".to_string()));
1063 assert_eq!(
1064 ctx.path(),
1065 &[
1066 "place_order".to_string(),
1067 "read_order_payload".to_string(),
1068 "parse_order".to_string()
1069 ]
1070 );
1071 assert_eq!(
1072 ctx.path_string().as_deref(),
1073 Some("place_order / read_order_payload / parse_order")
1074 );
1075 }
1076
1077 #[test]
1078 fn test_errcontext_from_string() {
1079 let ctx = CallContext::from(("key".to_string(), "test_string".to_string()));
1080 assert_eq!(ctx.items.len(), 1);
1081 assert_eq!(ctx.items[0], ("key".to_string(), "test_string".to_string()));
1082 }
1083
1084 #[test]
1085 fn test_errcontext_from_str() {
1086 let ctx = CallContext::from(("key", "test_str"));
1087 assert_eq!(ctx.items.len(), 1);
1088 assert_eq!(ctx.items[0], ("key".to_string(), "test_str".to_string()));
1089 }
1090
1091 #[test]
1092 fn test_errcontext_from_string_pair() {
1093 let ctx = CallContext::from(("key1".to_string(), "value1".to_string()));
1094 assert_eq!(ctx.items.len(), 1);
1095 assert_eq!(ctx.items[0], ("key1".to_string(), "value1".to_string()));
1096 }
1097
1098 #[test]
1099 fn test_errcontext_from_str_pair() {
1100 let ctx = CallContext::from(("key1", "value1"));
1101 assert_eq!(ctx.items.len(), 1);
1102 assert_eq!(ctx.items[0], ("key1".to_string(), "value1".to_string()));
1103 }
1104
1105 #[test]
1106 fn test_errcontext_from_mixed_pair() {
1107 let ctx = CallContext::from(("key1", "value1".to_string()));
1108 assert_eq!(ctx.items.len(), 1);
1109 assert_eq!(ctx.items[0], ("key1".to_string(), "value1".to_string()));
1110 }
1111
1112 #[test]
1113 fn test_errcontext_default() {
1114 let ctx = CallContext::default();
1115 assert_eq!(ctx.items.len(), 0);
1116 }
1117
1118 #[test]
1119 fn test_errcontext_display_single() {
1120 let ctx = CallContext::from(("key", "test"));
1121 let display = format!("{ctx}");
1122 assert!(display.contains("call context:"));
1123 assert!(display.contains("key : test"));
1124 }
1125
1126 #[test]
1127 fn test_errcontext_display_multiple() {
1128 let mut ctx = CallContext::default();
1129 ctx.items.push(("key1".to_string(), "value1".to_string()));
1130 ctx.items.push(("key2".to_string(), "value2".to_string()));
1131 let display = format!("{ctx}");
1132 assert!(display.contains("call context:"));
1133 assert!(display.contains("key1 : value1"));
1134 assert!(display.contains("key2 : value2"));
1135 }
1136
1137 #[test]
1138 fn test_errcontext_display_empty() {
1139 let ctx = CallContext::default();
1140 let display = format!("{ctx}");
1141 assert_eq!(display, "");
1142 }
1143
1144 #[test]
1145 fn test_withcontext_from_string() {
1146 let ctx = OperationContext::from("test_string".to_string());
1147 assert!(ctx.target.is_none());
1148 assert_eq!(ctx.context().items.len(), 1);
1149 assert_eq!(
1150 ctx.context().items[0],
1151 ("key".to_string(), "test_string".to_string())
1152 );
1153 }
1154
1155 #[test]
1156 fn test_withcontext_from_str() {
1157 let ctx = OperationContext::from("test_str".to_string());
1158 assert!(ctx.target.is_none());
1159 assert_eq!(ctx.context().items.len(), 1);
1160 assert_eq!(
1161 ctx.context().items[0],
1162 ("key".to_string(), "test_str".to_string())
1163 );
1164 }
1165
1166 #[test]
1167 fn test_withcontext_from_pathbuf() {
1168 let path = PathBuf::from("/test/path");
1169 let ctx = OperationContext::from(&path);
1170 assert!(ctx.target.is_none());
1171 assert_eq!(ctx.context().items.len(), 1);
1172 assert!(ctx.context().items[0].1.contains("/test/path"));
1173 }
1174
1175 #[test]
1176 fn test_withcontext_from_path() {
1177 let path = "/test/path";
1178 let ctx = OperationContext::from(path);
1179 assert!(ctx.target.is_none());
1180 assert_eq!(ctx.context().items.len(), 1);
1181 assert!(ctx.context().items[0].1.contains("/test/path"));
1182 }
1183
1184 #[test]
1185 fn test_withcontext_from_string_pair() {
1186 let ctx = OperationContext::from(("key1".to_string(), "value1".to_string()));
1187 assert!(ctx.target.is_none());
1188 assert_eq!(ctx.context().items.len(), 1);
1189 assert_eq!(
1190 ctx.context().items[0],
1191 ("key1".to_string(), "value1".to_string())
1192 );
1193 }
1194
1195 #[test]
1196 fn test_withcontext_from_str_pair() {
1197 let ctx = OperationContext::from(("key1", "value1"));
1198 assert!(ctx.target.is_none());
1199 assert_eq!(ctx.context().items.len(), 1);
1200 assert_eq!(
1201 ctx.context().items[0],
1202 ("key1".to_string(), "value1".to_string())
1203 );
1204 }
1205
1206 #[test]
1207 fn test_withcontext_from_mixed_pair() {
1208 let ctx = OperationContext::from(("key1", "value1".to_string()));
1209 assert!(ctx.target.is_none());
1210 assert_eq!(ctx.context().items.len(), 1);
1211 assert_eq!(
1212 ctx.context().items[0],
1213 ("key1".to_string(), "value1".to_string())
1214 );
1215 }
1216
1217 #[test]
1218 fn test_withcontext_from_path_pair() {
1219 let path = PathBuf::from("/test/path");
1220 let ctx = OperationContext::from(("file", path.to_string_lossy().as_ref()));
1221 assert!(ctx.target.is_none());
1222 assert_eq!(ctx.context().items.len(), 1);
1223 assert!(ctx.context().items[0].0.contains("file"));
1224 assert!(ctx.context().items[0].1.contains("/test/path"));
1225 }
1226
1227 #[test]
1228 fn test_withcontext_display_with_target() {
1229 let mut ctx = OperationContext::doing("test_target");
1230 ctx.record("key1", "value1");
1231 let display = format!("{ctx}");
1232 assert!(display.contains("doing: test_target"));
1233 assert!(display.contains("1. key1: value1"));
1234 }
1235
1236 #[test]
1237 fn test_withcontext_display_without_target() {
1238 let mut ctx = OperationContext::new();
1239 ctx.record("key1", "value1");
1240 let display = format!("{ctx}");
1241 assert!(!display.contains("target:"));
1242 assert!(display.contains("1. key1: value1"));
1243 }
1244
1245 #[test]
1246 fn test_withcontext_from_errcontext() {
1247 let err_ctx = CallContext::from(("key1", "value1"));
1248 let ctx = OperationContext::from(err_ctx);
1249 assert!(ctx.target.is_none());
1250 assert_eq!(ctx.context().items.len(), 1);
1251 assert_eq!(
1252 ctx.context().items[0],
1253 ("key1".to_string(), "value1".to_string())
1254 );
1255 }
1256
1257 #[test]
1258 fn test_withcontext_from_withcontext() {
1259 let mut ctx1 = OperationContext::doing("target1");
1260 ctx1.record("key1", "value1");
1261 ctx1.with_doing("step1");
1262 let ctx2 = OperationContext::from(&ctx1);
1263 assert_eq!(*ctx2.target(), Some("target1".to_string()));
1264 assert_eq!(ctx2.path(), &["target1".to_string(), "step1".to_string()]);
1265 assert_eq!(ctx2.context().items.len(), 1);
1266 assert_eq!(
1267 ctx2.context().items[0],
1268 ("key1".to_string(), "value1".to_string())
1269 );
1270 }
1271
1272 #[test]
1273 fn test_withcontext_from_str_path_pair() {
1274 let path = PathBuf::from("/test/path");
1275 let ctx = OperationContext::from(("file", &path));
1276 assert_eq!(ctx.context().items.len(), 1);
1277 assert_eq!(ctx.context().items[0].0, "file");
1278 assert!(ctx.context().items[0].1.contains("/test/path"));
1279 }
1280
1281 #[test]
1282 fn test_withcontext_from_str_pathbuf_pair() {
1283 let path = PathBuf::from("/test/pathbuf");
1284 let ctx = OperationContext::from(("file", path));
1285 assert_eq!(ctx.context().items.len(), 1);
1286 assert_eq!(ctx.context().items[0].0, "file");
1287 assert!(ctx.context().items[0].1.contains("/test/pathbuf"));
1288 }
1289
1290 #[test]
1294 fn test_withcontext_edge_cases() {
1295 let ctx1 = OperationContext::from("".to_string());
1296 assert_eq!(ctx1.context().items.len(), 1);
1297 assert_eq!(ctx1.context().items[0], ("key".to_string(), "".to_string()));
1298
1299 let ctx2 = OperationContext::from(("".to_string(), "".to_string()));
1300 assert_eq!(ctx2.context().items.len(), 1);
1301 assert_eq!(ctx2.context().items[0], ("".to_string(), "".to_string()));
1302 }
1303
1304 #[test]
1305 fn test_errcontext_equality() {
1306 let ctx1 = CallContext::from(("key1", "value1"));
1307 let ctx2 = CallContext::from(("key1", "value1"));
1308 let ctx3 = CallContext::from(("key1", "value2"));
1309
1310 assert_eq!(ctx1, ctx2);
1311 assert_ne!(ctx1, ctx3);
1312 }
1313
1314 #[test]
1315 fn test_withcontext_equality() {
1316 let ctx1 = OperationContext::from(("key1", "value1"));
1317 let ctx2 = OperationContext::from(("key1", "value1"));
1318 let ctx3 = OperationContext::from(("key1", "value2"));
1319
1320 assert_eq!(ctx1, ctx2);
1321 assert_ne!(ctx1, ctx3);
1322 }
1323
1324 #[test]
1325 fn test_withcontext_clone() {
1326 let mut ctx = OperationContext::doing("target");
1327 ctx.record("key", "value");
1328
1329 let cloned = ctx.clone();
1330 assert_eq!(ctx.target(), cloned.target());
1331 assert_eq!(ctx.context().items.len(), cloned.context().items.len());
1332 assert_eq!(ctx.context().items[0], cloned.context().items[0]);
1333 }
1334
1335 #[test]
1336 fn test_withcontext_with_types() {
1337 let mut ctx = OperationContext::new();
1338
1339 ctx.record("string_key", "string_value");
1341 ctx.record("string_key", 42.to_string()); ctx.record("bool_key", true.to_string()); assert_eq!(ctx.context().items.len(), 3);
1345
1346 assert_eq!(
1348 ctx.context().items[2],
1349 ("bool_key".to_string(), "true".to_string())
1350 );
1351 }
1352
1353 #[test]
1354 fn test_mark_suc() {
1355 let mut ctx = OperationContext::new();
1356 assert!(ctx.result == OperationResult::Fail);
1357
1358 ctx.mark_suc();
1359 assert!(ctx.result == OperationResult::Suc);
1360 }
1361
1362 #[test]
1363 fn test_with_exit_log() {
1364 let ctx = OperationContext::new().with_auto_log();
1365 assert!(ctx.exit_log);
1366
1367 let ctx2 = OperationContext::doing("test").with_auto_log();
1368 assert!(ctx2.exit_log);
1369 assert_eq!(*ctx2.target(), Some("test".to_string()));
1370 }
1371
1372 #[test]
1373 fn test_scope_marks_success() {
1374 let mut ctx = OperationContext::doing("scope_success");
1375 {
1376 let _scope = ctx.scoped_success();
1377 }
1378 assert!(matches!(ctx.result(), OperationResult::Suc));
1379 }
1380
1381 #[test]
1382 fn test_scope_preserves_failure() {
1383 let mut ctx = OperationContext::doing("scope_fail");
1384 {
1385 let mut scope = ctx.scoped_success();
1386 scope.mark_failure();
1387 }
1388 assert!(matches!(ctx.result(), OperationResult::Fail));
1389 }
1390
1391 #[test]
1392 fn test_scope_cancel() {
1393 let mut ctx = OperationContext::doing("scope_cancel");
1394 {
1395 let mut scope = ctx.scoped_success();
1396 scope.cancel();
1397 }
1398 assert!(matches!(ctx.result(), OperationResult::Cancel));
1399 }
1400
1401 #[test]
1402 fn test_format_context_with_target() {
1403 let mut ctx = OperationContext::doing("test_target");
1404 ctx.record("key1", "value1");
1405
1406 let formatted = ctx.format_context();
1407 assert_eq!(
1408 formatted,
1409 "doing=test_target: \ncall context:\n\tkey1 : value1\n"
1410 );
1411 }
1412
1413 #[test]
1414 fn test_format_context_without_target() {
1415 let mut ctx = OperationContext::new();
1416 ctx.record("key1", "value1");
1417
1418 let formatted = ctx.format_context();
1419 assert_eq!(formatted, "call context:\n\tkey1 : value1\n");
1420 }
1421
1422 #[test]
1423 fn test_format_context_empty() {
1424 let ctx = OperationContext::new();
1425 let formatted = ctx.format_context();
1426 assert_eq!(formatted, "");
1427 }
1428
1429 #[test]
1430 fn test_format_context_with_target_only() {
1431 let ctx = OperationContext::doing("test_target");
1432 let formatted = ctx.format_context();
1433 assert_eq!(formatted, "doing=test_target");
1434 }
1435
1436 #[test]
1437 fn test_format_context_with_path() {
1438 let mut ctx = OperationContext::doing("place_order");
1439 ctx.with_doing("read_order_payload");
1440 ctx.record("order_id", "42");
1441
1442 let formatted = ctx.format_context();
1443 assert_eq!(
1444 formatted,
1445 "doing=place_order path=place_order / read_order_payload: \ncall context:\n\torder_id : 42\n"
1446 );
1447 }
1448
1449 #[test]
1450 fn test_path_string_and_display_use_normalized_action_locator_order() {
1451 let mut ctx = OperationContext::at("engine.toml");
1452 ctx.with_doing("start engine");
1453
1454 assert_eq!(
1455 ctx.path_string().as_deref(),
1456 Some("start engine / engine.toml")
1457 );
1458 assert_eq!(
1459 ctx.path(),
1460 &["start engine".to_string(), "engine.toml".to_string()]
1461 );
1462
1463 let rendered = format!("{ctx}");
1464 assert!(rendered.contains("doing: start engine"));
1465 assert!(rendered.contains("at: engine.toml"));
1466 assert!(rendered.contains("path: start engine / engine.toml"));
1467 assert!(!rendered.contains("path: engine.toml / start engine"));
1468 }
1469
1470 #[test]
1471 fn test_logging_methods() {
1472 let ctx = OperationContext::doing("test_target");
1473
1474 ctx.info("info message");
1476 ctx.debug("debug message");
1477 ctx.warn("warn message");
1478 ctx.error("error message");
1479 ctx.trace("trace message");
1480 }
1481
1482 #[test]
1483 fn test_logging_methods_with_empty_context() {
1484 let ctx = OperationContext::new();
1485
1486 ctx.info("info message");
1488 ctx.debug("debug message");
1489 ctx.warn("warn message");
1490 ctx.error("error message");
1491 ctx.trace("trace message");
1492 }
1493
1494 #[test]
1495 fn test_context_add_trait() {
1496 let mut ctx = OperationContext::new();
1497
1498 ctx.add_context(("key1", "value1"));
1500 ctx.add_context(("key2", "value2"));
1501
1502 assert_eq!(ctx.context().items.len(), 2);
1503 assert_eq!(
1504 ctx.context().items[0],
1505 ("key1".to_string(), "value1".to_string())
1506 );
1507 assert_eq!(
1508 ctx.context().items[1],
1509 ("key2".to_string(), "value2".to_string())
1510 );
1511 }
1512
1513 #[test]
1514 fn test_drop_trait_with_success() {
1515 {
1516 let mut ctx = OperationContext::doing("test_drop").with_auto_log();
1517 ctx.record("operation", "test");
1518 ctx.mark_suc(); }
1521 }
1524
1525 #[test]
1526 fn test_drop_trait_with_failure() {
1527 {
1528 let mut ctx = OperationContext::doing("test_drop_fail").with_auto_log();
1529 ctx.record("operation", "test_fail");
1530 }
1533 }
1536
1537 #[test]
1538 fn test_drop_trait_without_exit_log() {
1539 {
1540 let mut ctx = OperationContext::doing("test_no_log");
1541 ctx.record("operation", "no_log");
1542 ctx.mark_suc();
1543 }
1546 }
1548
1549 #[test]
1550 fn test_complex_context_scenario() {
1551 let mut ctx = OperationContext::doing("user_registration").with_auto_log();
1553
1554 ctx.record("user_id", "12345");
1556 ctx.record("email", "test@example.com");
1557 ctx.record("role", "user");
1558
1559 ctx.info("开始用户注册流程");
1561 ctx.debug("验证用户输入");
1562 ctx.warn("检测到潜在的安全风险");
1563
1564 ctx.mark_suc();
1566 ctx.info("用户注册成功");
1567
1568 assert!(ctx.result == OperationResult::Suc);
1570 assert!(ctx.exit_log);
1571 assert_eq!(*ctx.target(), Some("user_registration".to_string()));
1572 assert_eq!(ctx.context().items.len(), 3);
1573
1574 let formatted = ctx.format_context();
1576 assert!(formatted.contains("user_registration"));
1577 assert!(formatted.contains("user_id"));
1578 assert!(formatted.contains("email"));
1579 assert!(formatted.contains("role"));
1580 }
1581
1582 #[test]
1583 fn test_context_with_special_characters() {
1584 let mut ctx = OperationContext::new();
1585
1586 ctx.record("key_with_spaces", "value with spaces");
1588 ctx.record("key_with_unicode", "值包含中文");
1589 ctx.record("key_with_symbols", "value@#$%^&*()");
1590
1591 assert_eq!(ctx.context().items.len(), 3);
1592 assert_eq!(
1593 ctx.context().items[0],
1594 (
1595 "key_with_spaces".to_string(),
1596 "value with spaces".to_string()
1597 )
1598 );
1599 assert_eq!(
1600 ctx.context().items[1],
1601 ("key_with_unicode".to_string(), "值包含中文".to_string())
1602 );
1603 assert_eq!(
1604 ctx.context().items[2],
1605 ("key_with_symbols".to_string(), "value@#$%^&*()".to_string())
1606 );
1607
1608 let display = format!("{ctx}");
1610 assert!(display.contains("key_with_spaces"));
1611 assert!(display.contains("值包含中文"));
1612 assert!(display.contains("value@#$%^&*()"));
1613 }
1614
1615 #[test]
1616 fn test_context_builder_pattern() {
1617 let ctx = OperationContext::doing("builder_test").with_auto_log();
1619
1620 assert_eq!(*ctx.target(), Some("builder_test".to_string()));
1621 assert_eq!(ctx.path(), &["builder_test".to_string()]);
1622 assert!(ctx.exit_log);
1623 }
1624
1625 #[test]
1626 fn test_context_multiple_with_calls() {
1627 let mut ctx = OperationContext::new();
1628
1629 ctx.record("key1", "value1");
1631 ctx.record("key2", "value2");
1632 ctx.record("key3", "value3");
1633 ctx.record("key1", "new_value1"); assert_eq!(ctx.context().items.len(), 4);
1637 assert_eq!(
1638 ctx.context().items[0],
1639 ("key1".to_string(), "value1".to_string())
1640 );
1641 assert_eq!(
1642 ctx.context().items[3],
1643 ("key1".to_string(), "new_value1".to_string())
1644 );
1645 }
1646
1647 #[test]
1648 fn test_context_from_various_types() {
1649 let ctx1 = OperationContext::from("simple_string");
1651 assert_eq!(
1652 ctx1.context().items[0],
1653 ("key".to_string(), "simple_string".to_string())
1654 );
1655
1656 let ctx2 = OperationContext::from(("custom_key", "custom_value"));
1657 assert_eq!(
1658 ctx2.context().items[0],
1659 ("custom_key".to_string(), "custom_value".to_string())
1660 );
1661
1662 let path = PathBuf::from("/test/path/file.txt");
1663 let ctx3 = OperationContext::from(&path);
1664 assert!(ctx3.context().items[0].0.contains("path"));
1665 assert!(ctx3.context().items[0].1.contains("/test/path/file.txt"));
1666 }
1667
1668 #[test]
1670 fn test_context_take_with_string_types() {
1671 let mut ctx = OperationContext::new();
1672
1673 ctx.record("string_key", "string_value");
1675 ctx.record("string_key2", String::from("string_value2"));
1676 ctx.record(String::from("string_key3"), "string_value3");
1677 ctx.record(String::from("string_key4"), String::from("string_value4"));
1678
1679 assert_eq!(ctx.context().items.len(), 4);
1680 assert_eq!(
1681 ctx.context().items[0],
1682 ("string_key".to_string(), "string_value".to_string())
1683 );
1684 assert_eq!(
1685 ctx.context().items[1],
1686 ("string_key2".to_string(), "string_value2".to_string())
1687 );
1688 assert_eq!(
1689 ctx.context().items[2],
1690 ("string_key3".to_string(), "string_value3".to_string())
1691 );
1692 assert_eq!(
1693 ctx.context().items[3],
1694 ("string_key4".to_string(), "string_value4".to_string())
1695 );
1696 }
1697
1698 #[test]
1699 fn test_context_take_with_numeric_types() {
1700 let mut ctx = OperationContext::new();
1701
1702 ctx.record("int_key", 42.to_string());
1704 ctx.record("float_key", 3.24.to_string());
1705 ctx.record("bool_key", true.to_string());
1706
1707 assert_eq!(ctx.context().items.len(), 3);
1708 assert_eq!(
1709 ctx.context().items[0],
1710 ("int_key".to_string(), "42".to_string())
1711 );
1712 assert_eq!(
1713 ctx.context().items[1],
1714 ("float_key".to_string(), "3.24".to_string())
1715 );
1716 assert_eq!(
1717 ctx.context().items[2],
1718 ("bool_key".to_string(), "true".to_string())
1719 );
1720 }
1721
1722 #[test]
1723 fn test_context_take_with_path_context() {
1724 let mut ctx = OperationContext::new();
1725
1726 let path1 = PathBuf::from("/test/path1.txt");
1728 let path2 = Path::new("/test/path2.txt");
1729
1730 ctx.record("file1", &path1);
1731 ctx.record("file2", path2);
1732
1733 assert_eq!(ctx.context().items.len(), 2);
1734 assert_eq!(ctx.context().items[0].0, "file1");
1735 assert!(ctx.context().items[0].1.contains("/test/path1.txt"));
1736 assert_eq!(ctx.context().items[1].0, "file2");
1737 assert!(ctx.context().items[1].1.contains("/test/path2.txt"));
1738 }
1739
1740 #[test]
1741 fn test_context_take_mixed_types() {
1742 let mut ctx = OperationContext::new();
1743
1744 ctx.record("name", "test_user");
1746 ctx.record("age", 25.to_string());
1747 ctx.record("config_file", &PathBuf::from("/etc/config.toml"));
1748 ctx.record("status", "active");
1749
1750 assert_eq!(ctx.context().items.len(), 4);
1751 assert_eq!(
1752 ctx.context().items[0],
1753 ("name".to_string(), "test_user".to_string())
1754 );
1755 assert_eq!(
1756 ctx.context().items[1],
1757 ("age".to_string(), "25".to_string())
1758 );
1759 assert_eq!(ctx.context().items[2].0, "config_file");
1760 assert!(ctx.context().items[2].1.contains("/etc/config.toml"));
1761 assert_eq!(
1762 ctx.context().items[3],
1763 ("status".to_string(), "active".to_string())
1764 );
1765 }
1766
1767 #[test]
1768 fn test_context_take_edge_cases() {
1769 let mut ctx = OperationContext::new();
1770
1771 ctx.record("", ""); ctx.record("empty_value", ""); ctx.record("", "empty_key"); ctx.record("special_chars", "@#$%^&*()"); ctx.record("unicode", "测试中文字符"); assert_eq!(ctx.context().items.len(), 5);
1779 assert_eq!(ctx.context().items[0], ("".to_string(), "".to_string()));
1780 assert_eq!(
1781 ctx.context().items[1],
1782 ("empty_value".to_string(), "".to_string())
1783 );
1784 assert_eq!(
1785 ctx.context().items[2],
1786 ("".to_string(), "empty_key".to_string())
1787 );
1788 assert_eq!(
1789 ctx.context().items[3],
1790 ("special_chars".to_string(), "@#$%^&*()".to_string())
1791 );
1792 assert_eq!(
1793 ctx.context().items[4],
1794 ("unicode".to_string(), "测试中文字符".to_string())
1795 );
1796 }
1797
1798 #[test]
1799 fn test_context_take_multiple_calls() {
1800 let mut ctx = OperationContext::new();
1801
1802 ctx.record("key1", "value1");
1804 ctx.record("key2", "value2");
1805 ctx.record("key1", "new_value1"); ctx.record("key3", &PathBuf::from("/path/file.txt"));
1807 ctx.record("key2", &PathBuf::from("/path/file2.txt")); assert_eq!(ctx.context().items.len(), 5);
1811 assert_eq!(
1812 ctx.context().items[0],
1813 ("key1".to_string(), "value1".to_string())
1814 );
1815 assert_eq!(
1816 ctx.context().items[1],
1817 ("key2".to_string(), "value2".to_string())
1818 );
1819 assert_eq!(
1820 ctx.context().items[2],
1821 ("key1".to_string(), "new_value1".to_string())
1822 );
1823 assert_eq!(ctx.context().items[3].0, "key3");
1824 assert!(ctx.context().items[3].1.contains("/path/file.txt"));
1825 assert_eq!(ctx.context().items[4].0, "key2");
1826 assert!(ctx.context().items[4].1.contains("/path/file2.txt"));
1827 }
1828
1829 #[test]
1830 fn test_context_take_with_existing_context() {
1831 let mut ctx = OperationContext::from(("existing_key", "existing_value"));
1833
1834 ctx.record("new_key1", "new_value1");
1836 ctx.record("new_key2", &PathBuf::from("/new/path.txt"));
1837
1838 assert_eq!(ctx.context().items.len(), 3);
1839 assert_eq!(
1840 ctx.context().items[0],
1841 ("existing_key".to_string(), "existing_value".to_string())
1842 );
1843 assert_eq!(
1844 ctx.context().items[1],
1845 ("new_key1".to_string(), "new_value1".to_string())
1846 );
1847 assert_eq!(ctx.context().items[2].0, "new_key2");
1848 assert!(ctx.context().items[2].1.contains("/new/path.txt"));
1849 }
1850
1851 #[test]
1852 fn test_context_metadata_records_values() {
1853 let ctx = OperationContext::doing("load")
1854 .with_meta("config.kind", "wpsrc")
1855 .with_meta("parse.line", 1u32)
1856 .with_meta("parse.strict", true);
1857
1858 assert_eq!(ctx.metadata().get_str("config.kind"), Some("wpsrc"));
1859 assert!(ctx.metadata().as_map().contains_key("parse.line"));
1860 assert!(ctx.metadata().as_map().contains_key("parse.strict"));
1861 }
1862
1863 #[test]
1864 fn test_context_metadata_duplicate_key_overwrites() {
1865 let ctx = OperationContext::new()
1866 .with_meta("config.kind", "sink_route")
1867 .with_meta("config.kind", "sink_defaults");
1868
1869 assert_eq!(ctx.metadata().get_str("config.kind"), Some("sink_defaults"));
1870 }
1871
1872 #[test]
1873 fn test_context_metadata_ignores_empty_key() {
1874 let result = std::panic::catch_unwind(|| OperationContext::new().with_meta("", "ignored"));
1875
1876 if cfg!(debug_assertions) {
1877 assert!(result.is_err());
1878 } else {
1879 let ctx = result.expect("release build should ignore empty metadata key");
1880 assert!(ctx.metadata().is_empty());
1881 }
1882 }
1883
1884}