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