use std::{
io::Write,
path::{Path, PathBuf},
};
use clap::{Parser, ValueEnum};
use super::{AmbientError, Config, Leaf};
use ambient_ci::{
project::{ProjectError, State},
runlog::{RunLog, RunLogError, SynthLog},
};
#[derive(Debug, Parser)]
pub struct Log {
#[clap(conflicts_with = "format", long)]
console: bool,
#[clap(long, default_value = "raw")]
format: Format,
#[clap(conflicts_with = "filename")]
project: Option<String>,
#[clap(long)]
filename: Option<PathBuf>,
#[clap(long)]
output: Option<PathBuf>,
}
impl Log {
fn state(&self, config: &Config, project: &str) -> Result<State, LogError> {
let statedir = config.state();
if !statedir.exists() {
return Err(LogError::NoStateDir(project.to_string(), statedir.into()))?;
}
State::from_file(statedir, project).map_err(LogError::Project)
}
fn read(&self, filename: &Path) -> Result<Vec<u8>, LogError> {
std::fs::read(filename).map_err(|err| LogError::Read(filename.to_path_buf(), err))
}
fn write(&self, data: &[u8]) -> Result<(), LogError> {
if let Some(output) = &self.output {
std::fs::write(output, data)
.map_err(|err| LogError::Write(output.to_path_buf(), err))?;
} else {
std::io::stdout()
.write_all(data)
.map_err(LogError::WriteStdout)?;
}
Ok(())
}
}
impl Leaf for Log {
fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
match (self.format, self.console, &self.project, &self.filename) {
(Format::Raw, false, Some(project), None) => {
let state = self.state(config, project)?;
let data = self.read(&state.raw_log_filename())?;
self.write(&data)?;
}
(Format::Raw, false, None, Some(filename)) => {
let data = self.read(filename)?;
self.write(&data)?;
}
(Format::Raw, true, Some(project), None) => {
let state = self.state(config, project)?;
let data = self.read(&state.console_log_filename())?;
self.write(&data)?;
}
(Format::Raw, true, None, Some(filename)) => {
let data = self.read(filename)?;
self.write(&data)?;
}
(Format::Json, false, Some(project), None) => {
let state = self.state(config, project)?;
let data = self.read(&state.raw_log_filename())?;
self.write(&data)?;
}
(Format::Json, false, None, Some(filename)) => {
let data = self.read(filename)?;
self.write(&data)?;
}
(Format::Html, false, Some(project), None) => {
let state = self.state(config, project)?;
let data = self.read(&state.run_log_filename())?;
let runlog = RunLog::parse_jsonl(data)
.map_err(|err| LogError::LoadJson(state.run_log_filename(), err))?;
let mut synth = SynthLog::new(runlog.msgs());
let mut data = self.read(&state.console_log_filename())?;
data.retain(|byte| *byte != b'\r' && *byte != b'\x1b');
synth.set_console_log(data);
self.write(synth.to_html().to_string().as_bytes())?;
}
(_, _, None, None)
| (_, _, Some(_), Some(_))
| (Format::Json, true, _, _)
| (Format::Html, true, _, _)
| (Format::Html, false, None, Some(_)) => {
Err(LogError::Usage)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Format {
Raw,
Json,
Html,
}
#[derive(Debug, thiserror::Error)]
pub enum LogError {
#[error("state directory for project {0} does not exist: {1}")]
NoStateDir(String, PathBuf),
#[error(transparent)]
Project(#[from] ProjectError),
#[error("failed to load Ambient JSON run log file {0}")]
LoadJson(PathBuf, #[source] RunLogError),
#[error("failed to read log file {0}")]
Read(PathBuf, #[source] std::io::Error),
#[error("failed to write output to {0}")]
Write(PathBuf, #[source] std::io::Error),
#[error("failed to write output to stdout")]
WriteStdout(#[source] std::io::Error),
#[error(
"the combination of output format, console log, project, and file names is not acceptable"
)]
Usage,
}