use owo_colors::Stream;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Output {
Stdout,
Stderr,
File(PathBuf),
}
impl Output {
pub fn file<P: AsRef<Path>>(path: P) -> Self {
Output::File(path.as_ref().to_path_buf())
}
pub fn as_str(&self) -> &str {
match self {
Output::Stdout => "stdout",
Output::Stderr => "stderr",
Output::File(_) => "file",
}
}
pub fn is_file(&self) -> bool {
matches!(self, Output::File(_))
}
pub fn file_path(&self) -> Option<&Path> {
match self {
Output::File(path) => Some(path),
_ => None,
}
}
}
impl Default for Output {
fn default() -> Self {
Self::Stdout
}
}
impl fmt::Display for Output {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Output::Stdout => write!(f, "stdout"),
Output::Stderr => write!(f, "stderr"),
Output::File(path) => write!(f, "file:{}", path.display()),
}
}
}
impl FromStr for Output {
type Err = ParseOutputError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"stdout" => Ok(Output::Stdout),
"stderr" => Ok(Output::Stderr),
_ if s.starts_with("file:") => {
let path = &s[5..];
Ok(Output::File(PathBuf::from(path)))
}
_ => {
Ok(Output::File(PathBuf::from(s)))
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseOutputError {
invalid_input: String,
}
impl fmt::Display for ParseOutputError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid output destination '{}', expected: stdout, stderr, or a file path",
self.invalid_input
)
}
}
impl std::error::Error for ParseOutputError {}
impl From<&Output> for Stream {
fn from(output: &Output) -> Self {
match output {
Output::Stdout | Output::File(_) => Stream::Stdout,
Output::Stderr => Stream::Stderr,
}
}
}
pub mod compat {
pub const STDOUT: &str = "stdout";
pub const STDERR: &str = "stderr";
#[deprecated(since = "0.6.0", note = "Use Output::Stdout instead")]
pub fn stdout() -> Option<String> {
Some(String::from(STDOUT))
}
#[deprecated(since = "0.6.0", note = "Use Output::Stderr instead")]
pub fn stderr() -> Option<String> {
Some(String::from(STDERR))
}
}
pub use compat::{STDERR, STDOUT};
#[cfg(test)]
mod tests {
use super::{compat, Output, Stream, STDERR, STDOUT};
use std::path::{Path, PathBuf};
#[test]
fn test_output_default() {
assert_eq!(Output::default(), Output::Stdout);
}
#[test]
fn test_output_file() {
let output = Output::file("/var/log/app.log");
assert!(output.is_file());
assert_eq!(output.file_path().unwrap(), Path::new("/var/log/app.log"));
}
#[test]
fn test_output_is_file() {
assert!(!Output::Stdout.is_file());
assert!(!Output::Stderr.is_file());
assert!(Output::File(PathBuf::from("/tmp/test.log")).is_file());
}
#[test]
fn test_output_file_path() {
assert_eq!(Output::Stdout.file_path(), None);
assert_eq!(Output::Stderr.file_path(), None);
let path = PathBuf::from("/tmp/test.log");
let output = Output::File(path.clone());
assert_eq!(output.file_path(), Some(path.as_path()));
}
#[test]
fn test_output_display() {
assert_eq!(Output::Stdout.to_string(), "stdout");
assert_eq!(Output::Stderr.to_string(), "stderr");
assert_eq!(
Output::File(PathBuf::from("/tmp/test.log")).to_string(),
"file:/tmp/test.log"
);
}
#[test]
fn test_output_from_str() {
assert_eq!("stdout".parse::<Output>().unwrap(), Output::Stdout);
assert_eq!("stderr".parse::<Output>().unwrap(), Output::Stderr);
assert_eq!("STDOUT".parse::<Output>().unwrap(), Output::Stdout);
assert_eq!("STDERR".parse::<Output>().unwrap(), Output::Stderr);
}
#[test]
fn test_output_from_str_file() {
let result = "/tmp/test.log".parse::<Output>().unwrap();
assert_eq!(result, Output::File(PathBuf::from("/tmp/test.log")));
let result = "file:/var/log/app.log".parse::<Output>().unwrap();
assert_eq!(result, Output::File(PathBuf::from("/var/log/app.log")));
}
#[test]
fn test_output_to_stream() {
let stdout_stream = Stream::from(&Output::Stdout);
let stderr_stream = Stream::from(&Output::Stderr);
let file_stream = Stream::from(&Output::File(PathBuf::from("/tmp/test.log")));
match stdout_stream {
Stream::Stdout => {}
_ => panic!("Expected Stream::Stdout"),
}
match stderr_stream {
Stream::Stderr => {}
_ => panic!("Expected Stream::Stderr"),
}
match file_stream {
Stream::Stdout => {} _ => panic!("Expected Stream::Stdout for file"),
}
}
#[test]
fn test_output_eq() {
assert_eq!(Output::Stdout, Output::Stdout);
assert_ne!(Output::Stdout, Output::Stderr);
assert_eq!(
Output::File(PathBuf::from("/tmp/a.log")),
Output::File(PathBuf::from("/tmp/a.log"))
);
assert_ne!(
Output::File(PathBuf::from("/tmp/a.log")),
Output::File(PathBuf::from("/tmp/b.log"))
);
}
#[test]
fn test_output_clone() {
let output = Output::File(PathBuf::from("/tmp/test.log"));
let cloned = output.clone();
assert_eq!(output, cloned);
}
#[test]
fn test_output_serialize_deserialize() {
let stdout = Output::Stdout;
let serialized = serde_json::to_string(&stdout).unwrap();
assert_eq!(serialized, r#""stdout""#);
let deserialized: Output = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, Output::Stdout);
let file = Output::File(PathBuf::from("/tmp/test.log"));
let serialized = serde_json::to_string(&file).unwrap();
let deserialized: Output = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, file);
}
#[test]
fn test_output_as_str() {
assert_eq!(Output::Stdout.as_str(), "stdout");
assert_eq!(Output::Stderr.as_str(), "stderr");
assert_eq!(
Output::File(PathBuf::from("/tmp/test.log")).as_str(),
"file"
);
}
#[test]
fn test_compat_constants() {
assert_eq!(STDOUT, "stdout");
assert_eq!(STDERR, "stderr");
}
#[test]
#[allow(deprecated)]
fn test_compat_functions() {
assert_eq!(compat::stdout().unwrap(), "stdout");
assert_eq!(compat::stderr().unwrap(), "stderr");
}
}