use crate::runtime_manifest::RuntimeManifestError;
pub const TRACE_CAPTURE_ACCEPTED_VALUES: &[&str] = &["none", "meta", "content"];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TraceCapture {
None,
#[default]
Meta,
Content,
}
impl TraceCapture {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Meta => "meta",
Self::Content => "content",
}
}
#[must_use]
pub fn captures_content(self) -> bool {
matches!(self, Self::Content)
}
#[must_use]
pub fn captures_hashes(self) -> bool {
!matches!(self, Self::None)
}
}
impl std::str::FromStr for TraceCapture {
type Err = ParseTraceCaptureError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim() {
"none" => Ok(Self::None),
"meta" => Ok(Self::Meta),
"content" => Ok(Self::Content),
other => Err(ParseTraceCaptureError {
value: other.to_string(),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseTraceCaptureError {
pub value: String,
}
impl std::fmt::Display for ParseTraceCaptureError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"must be one of: {}; got '{}'",
TRACE_CAPTURE_ACCEPTED_VALUES.join(", "),
self.value
)
}
}
impl std::error::Error for ParseTraceCaptureError {}
pub fn resolve_trace_capture(
capture: Option<&str>,
include_tool_output: Option<bool>,
) -> Result<TraceCapture, RuntimeManifestError> {
match (capture, include_tool_output) {
(Some(_), Some(_)) => Err(RuntimeManifestError::InvalidTraceConfig {
field: "trace.include_tool_output".to_string(),
message: "'trace.capture' and 'trace.include_tool_output' are both set; \
'trace.include_tool_output' is retired — remove it and keep 'trace.capture'"
.to_string(),
}),
(Some(value), None) => value.parse().map_err(|e: ParseTraceCaptureError| {
RuntimeManifestError::InvalidTraceConfig {
field: "trace.capture".to_string(),
message: e.to_string(),
}
}),
(None, Some(flag)) => {
let replacement = if flag {
TraceCapture::Content
} else {
TraceCapture::Meta
};
eprintln!(
"warning: 'trace.include_tool_output: {flag}' is retired; use \
'trace.capture: {}' instead",
replacement.as_str()
);
Ok(replacement)
}
(None, None) => Ok(TraceCapture::default()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn absent_keys_resolve_to_meta() {
assert_eq!(
resolve_trace_capture(None, None).unwrap(),
TraceCapture::Meta
);
assert_eq!(TraceCapture::default(), TraceCapture::Meta);
}
#[test]
fn each_capture_name_parses() {
assert_eq!(
resolve_trace_capture(Some("none"), None).unwrap(),
TraceCapture::None
);
assert_eq!(
resolve_trace_capture(Some("meta"), None).unwrap(),
TraceCapture::Meta
);
assert_eq!(
resolve_trace_capture(Some("content"), None).unwrap(),
TraceCapture::Content
);
}
#[test]
fn include_tool_output_aliases_content_and_meta() {
assert_eq!(
resolve_trace_capture(None, Some(true)).unwrap(),
TraceCapture::Content
);
assert_eq!(
resolve_trace_capture(None, Some(false)).unwrap(),
TraceCapture::Meta
);
}
#[test]
fn both_keys_are_refused_even_when_they_agree() {
let err = resolve_trace_capture(Some("content"), Some(true)).unwrap_err();
let rendered = err.to_string();
assert!(rendered.contains("trace.capture"), "{rendered}");
assert!(rendered.contains("trace.include_tool_output"), "{rendered}");
}
#[test]
fn unknown_capture_value_names_the_field_and_the_accepted_values() {
let err = resolve_trace_capture(Some("verbose"), None).unwrap_err();
let rendered = err.to_string();
assert!(rendered.contains("trace.capture"), "{rendered}");
for accepted in TRACE_CAPTURE_ACCEPTED_VALUES {
assert!(rendered.contains(accepted), "{rendered}");
}
assert!(rendered.contains("verbose"), "{rendered}");
}
}