auv-cli-invoke 0.0.22

Application Use Via... script-able OS automation library
use auv_tracing::{ArtifactMetadata, EmitBytesOptions, EventPayload, NewArtifact};
use image::{ExtendedColorType, ImageEncoder, RgbaImage, codecs::png::PngEncoder};

#[derive(serde::Serialize)]
struct ArtifactPreparationFailed {
  purpose: String,
  error: String,
}

impl EventPayload for ArtifactPreparationFailed {
  const NAME: &'static str = "auv.invoke.artifact_preparation_failed";
  const VERSION: u32 = 1;
}

pub(crate) fn emit_png(purpose: &str, image: &RgbaImage) {
  if !auv_tracing::Context::current().can_publish_artifacts() {
    return;
  }
  match prepare_png(purpose, image) {
    Ok(emission) => drop(emission),
    Err(error) => emit_preparation_failure(purpose, error),
  }
}

pub(crate) async fn emit_png_with_receipt(purpose: &str, image: &RgbaImage) -> Option<ArtifactMetadata> {
  if !auv_tracing::Context::current().can_publish_artifacts() {
    return None;
  }
  let emission = match prepare_png(purpose, image) {
    Ok(emission) => emission,
    Err(error) => {
      emit_preparation_failure(purpose, error);
      return None;
    }
  };
  match emission.await {
    Ok(metadata) => metadata,
    Err(error) => {
      emit_preparation_failure(purpose, error.to_string());
      None
    }
  }
}

fn prepare_png(purpose: &str, image: &RgbaImage) -> Result<auv_tracing::ArtifactEmission, String> {
  let mut body = Vec::new();
  PngEncoder::new(&mut body)
    .write_image(image.as_raw(), image.width(), image.height(), ExtendedColorType::Rgba8)
    .map_err(|error| format!("failed to encode {purpose} PNG artifact: {error}"))
    .and_then(|()| {
      let options = EmitBytesOptions::new().with_purpose(purpose).with_content_type("image/png").with_file_extension("png");
      auv_tracing::emit_bytes_artifact(options, body).map_err(|error| format!("invalid {purpose} artifact bytes: {error}"))
    })
}

fn emit_preparation_failure(purpose: &str, error: String) {
  auv_tracing::emit_event!(ArtifactPreparationFailed {
    purpose: purpose.to_string(),
    error,
  });
}

pub(crate) async fn emit_bytes_with_receipt(options: EmitBytesOptions, body: Vec<u8>) -> Option<ArtifactMetadata> {
  if !auv_tracing::Context::current().can_publish_artifacts() {
    return None;
  }
  let purpose = options.purpose().to_string();
  let emission = match auv_tracing::emit_bytes_artifact(options, body).map_err(|error| format!("invalid {purpose} artifact bytes: {error}"))
  {
    Ok(emission) => emission,
    Err(error) => {
      auv_tracing::emit_event!(ArtifactPreparationFailed { purpose, error });
      return None;
    }
  };
  emission.await.ok().flatten()
}

pub(crate) fn emit_prepared<R>(purpose: &str, artifact: Result<NewArtifact<R>, String>)
where
  R: futures_util::io::AsyncRead + Unpin + Send + 'static,
{
  if !auv_tracing::Context::current().can_publish_artifacts() {
    return;
  }
  match artifact {
    Ok(artifact) => drop(auv_tracing::emit_artifact!(artifact)),
    Err(error) => auv_tracing::emit_event!(ArtifactPreparationFailed {
      purpose: purpose.to_string(),
      error,
    }),
  }
}

pub(crate) async fn emit_prepared_with_receipt<R>(purpose: &str, artifact: Result<NewArtifact<R>, String>) -> Option<ArtifactMetadata>
where
  R: futures_util::io::AsyncRead + Unpin + Send + 'static,
{
  if !auv_tracing::Context::current().can_publish_artifacts() {
    return None;
  }
  let emission = match artifact {
    Ok(artifact) => auv_tracing::emit_artifact!(artifact),
    Err(error) => {
      emit_preparation_failure(purpose, error);
      return None;
    }
  };
  match emission.await {
    Ok(metadata) => metadata,
    Err(error) => {
      emit_preparation_failure(purpose, error.to_string());
      None
    }
  }
}

#[cfg(test)]
#[path = "artifact_test.rs"]
mod tests;