use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ProjectOption {
pub id: String,
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct ProjectValue {
pub field: String,
pub kind: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub option: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Record {
ProjectBoard {
board: String,
number: i64,
title: String,
owner: String,
url: String,
closed: bool,
},
ProjectField {
board: String,
field: String,
name: String,
kind: String,
options: Vec<ProjectOption>,
},
ProjectItem {
board: String,
item: String,
kind: String,
title: String,
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
repository: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
number: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
state: Option<String>,
archived: bool,
body: String,
fields: Vec<ProjectValue>,
},
ProjectItemRemoved { board: String, item: String },
BoardReference { project: String, board: String },
Issue {
repo: String,
number: i64,
title: String,
state: String,
url: String,
updated_at: String,
},
SyncScope {
scope: String,
outcome: String,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
SyncProblem { problem: String },
SyncSummary {
changed: usize,
unchanged: usize,
failed: usize,
no_token: bool,
problems: usize,
},
Status {
mirror: String,
token: Option<String>,
scopes: usize,
failing: usize,
never_synced: usize,
},
Message { level: String, text: String },
Failure { message: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Stream {
Out,
Err,
}
type Host = Box<dyn Fn(Stream, &str) + Send + Sync>;
#[derive(Serialize)]
struct Line<'a> {
seq: u64,
ts: String,
#[serde(flatten)]
record: &'a Record,
}
pub struct Output {
jsonl: bool,
seq: AtomicU64,
host: Option<Host>,
}
impl Output {
pub fn new(jsonl: bool) -> Self {
Self {
jsonl,
seq: AtomicU64::new(0),
host: None,
}
}
pub fn through(host: impl Fn(Stream, &str) + Send + Sync + 'static) -> Self {
Self {
jsonl: false,
seq: AtomicU64::new(0),
host: Some(Box::new(host)),
}
}
fn write(&self, stream: Stream, line: &str) {
match (&self.host, stream) {
(Some(host), _) => host(stream, line),
(None, Stream::Out) => {
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{line}");
}
(None, Stream::Err) => eprintln!("{line}"),
}
}
pub fn is_jsonl(&self) -> bool {
self.jsonl
}
pub fn emit(&self, record: Record) {
if !self.jsonl {
return;
}
self.line(&record);
}
fn line(&self, record: &Record) {
let line = Line {
seq: self.seq.fetch_add(1, Ordering::SeqCst) + 1,
ts: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
record,
};
let text = serde_json::to_string(&line).expect("a record always serialises");
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "{text}");
let _ = stdout.flush();
}
pub fn say(&self, lines: impl IntoIterator<Item = String>) {
if self.jsonl {
return;
}
for line in lines {
self.write(Stream::Out, &line);
}
}
pub fn explain(&self, text: &str) {
if self.jsonl {
return;
}
for line in text.lines() {
self.write(Stream::Err, line);
}
}
pub fn warn(&self, text: &str) {
if self.jsonl {
self.line(&Record::Message {
level: "warning".to_string(),
text: text.to_string(),
});
} else {
self.write(Stream::Err, &format!("wsb github: {text}"));
}
}
pub fn fail(&self, message: &str) {
if self.jsonl {
self.line(&Record::Failure {
message: message.to_string(),
});
} else {
self.write(Stream::Err, &format!("wsb github: {message}"));
}
}
}