use serde::Serialize;
use crate::actions;
use crate::error::Result;
use crate::safe_println;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Format {
#[default]
Human,
Json,
Github,
}
impl Format {
pub fn as_str(self) -> &'static str {
match self {
Format::Human => "human",
Format::Json => "json",
Format::Github => "github",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Report {
pub ok: bool,
pub skipped: bool,
pub message_id: Option<i64>,
pub http_status: u16,
pub error: Option<String>,
pub chat_id: String,
pub attempts: u32,
pub dry_run: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub request: Option<serde_json::Value>,
}
impl Report {
pub fn skipped(chat_id: impl Into<String>) -> Self {
Report {
ok: false,
skipped: true,
message_id: None,
http_status: 0,
error: None,
chat_id: chat_id.into(),
attempts: 0,
dry_run: false,
request: None,
}
}
pub fn emit(&self, format: Format) -> Result<()> {
match format {
Format::Human => self.emit_human(),
Format::Json => self.emit_json(),
Format::Github => self.emit_github(),
}
}
fn emit_human(&self) -> Result<()> {
if self.skipped {
safe_println!("skipped: status not in notify_on");
return Ok(());
}
if self.dry_run {
safe_println!("dry-run: nothing sent to {}", self.chat_id);
if let Some(request) = &self.request {
safe_println!("{}", serde_json::to_string_pretty(request).unwrap_or_default());
}
return Ok(());
}
match (self.ok, &self.error) {
(true, _) => safe_println!(
"sent to {} (message_id={})",
self.chat_id,
self.message_id.map(|m| m.to_string()).unwrap_or_else(|| "?".into())
),
(false, Some(err)) => {
safe_println!("failed after {} attempt(s): {}", self.attempts, err)
}
(false, None) => safe_println!("failed after {} attempt(s)", self.attempts),
}
Ok(())
}
fn emit_json(&self) -> Result<()> {
let rendered = serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string());
safe_println!("{}", rendered);
Ok(())
}
fn emit_github(&self) -> Result<()> {
actions::set_output("ok", if self.ok { "true" } else { "false" })?;
actions::set_output(
"message_id",
&self.message_id.map(|m| m.to_string()).unwrap_or_default(),
)?;
actions::set_output("http_status", &self.http_status.to_string())?;
actions::set_output("error", self.error.as_deref().unwrap_or(""))?;
actions::step_summary(&self.summary_markdown())?;
Ok(())
}
pub fn summary_markdown(&self) -> String {
let state = if self.skipped {
"skipped"
} else if self.dry_run {
"dry-run"
} else if self.ok {
"sent"
} else {
"failed"
};
let mut out = String::from("### notiflow\n\n| Field | Value |\n|---|---|\n");
out.push_str(&format!("| result | {state} |\n"));
out.push_str(&format!("| chat_id | `{}` |\n", self.chat_id));
if let Some(id) = self.message_id {
out.push_str(&format!("| message_id | `{id}` |\n"));
}
out.push_str(&format!("| http_status | {} |\n", self.http_status));
out.push_str(&format!("| attempts | {} |\n", self.attempts));
if let Some(err) = &self.error {
out.push_str(&format!("| error | {err} |\n"));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sent() -> Report {
Report {
ok: true,
skipped: false,
message_id: Some(42),
http_status: 200,
error: None,
chat_id: "-100123".into(),
attempts: 1,
dry_run: false,
request: None,
}
}
#[test]
fn json_shape_is_stable() {
let v: serde_json::Value = serde_json::to_value(sent()).unwrap();
assert_eq!(v["ok"], serde_json::json!(true));
assert_eq!(v["message_id"], serde_json::json!(42));
assert_eq!(v["http_status"], serde_json::json!(200));
assert_eq!(v["error"], serde_json::Value::Null);
}
#[test]
fn summary_reports_each_state() {
assert!(sent().summary_markdown().contains("| result | sent |"));
assert!(Report::skipped("-1").summary_markdown().contains("| result | skipped |"));
let mut failed = sent();
failed.ok = false;
failed.error = Some("Bad Request: chat not found".into());
let md = failed.summary_markdown();
assert!(md.contains("| result | failed |"));
assert!(md.contains("chat not found"));
}
#[test]
fn skip_has_no_message_id_and_zero_status() {
let r = Report::skipped("@chan_name");
assert!(!r.ok);
assert!(r.skipped);
assert_eq!(r.http_status, 0);
assert!(r.message_id.is_none());
}
}