1use std::io::Write;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15use serde::Serialize;
16
17#[derive(Debug, Clone, PartialEq, Serialize)]
19pub struct ProjectOption {
20 pub id: String,
21 pub name: String,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize)]
26pub struct ProjectValue {
27 pub field: String,
29 pub kind: String,
31 pub value: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub option: Option<String>,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize)]
40#[serde(tag = "type", rename_all = "snake_case")]
41pub enum Record {
42 ProjectBoard {
45 board: String,
46 number: i64,
47 title: String,
48 owner: String,
49 url: String,
50 closed: bool,
51 },
52 ProjectField {
54 board: String,
55 field: String,
56 name: String,
57 kind: String,
58 options: Vec<ProjectOption>,
59 },
60 ProjectItem {
64 board: String,
65 item: String,
66 kind: String,
68 title: String,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 url: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 repository: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 number: Option<i64>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 state: Option<String>,
77 archived: bool,
78 body: String,
80 fields: Vec<ProjectValue>,
81 },
82 ProjectItemRemoved { board: String, item: String },
84 BoardReference { project: String, board: String },
86 Issue {
88 repo: String,
89 number: i64,
90 title: String,
91 state: String,
92 url: String,
93 updated_at: String,
94 },
95 SyncScope {
97 scope: String,
98 outcome: String,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 error: Option<String>,
102 },
103 SyncProblem { problem: String },
105 SyncSummary {
107 changed: usize,
108 unchanged: usize,
109 failed: usize,
110 no_token: bool,
111 problems: usize,
112 },
113 Status {
115 mirror: String,
116 token: Option<String>,
117 scopes: usize,
118 failing: usize,
119 never_synced: usize,
120 },
121 Message { level: String, text: String },
123 Failure { message: String },
125}
126
127#[derive(Serialize)]
128struct Line<'a> {
129 seq: u64,
130 ts: String,
131 #[serde(flatten)]
132 record: &'a Record,
133}
134
135pub struct Output {
137 jsonl: bool,
138 seq: AtomicU64,
139}
140
141impl Output {
142 pub fn new(jsonl: bool) -> Self {
143 Self {
144 jsonl,
145 seq: AtomicU64::new(0),
146 }
147 }
148
149 pub fn is_jsonl(&self) -> bool {
150 self.jsonl
151 }
152
153 pub fn emit(&self, record: Record) {
156 if !self.jsonl {
157 return;
158 }
159 self.line(&record);
160 }
161
162 fn line(&self, record: &Record) {
163 let line = Line {
164 seq: self.seq.fetch_add(1, Ordering::SeqCst) + 1,
165 ts: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
166 record,
167 };
168 let text = serde_json::to_string(&line).expect("a record always serialises");
169 let mut stdout = std::io::stdout().lock();
170 let _ = writeln!(stdout, "{text}");
171 let _ = stdout.flush();
172 }
173
174 pub fn say(&self, lines: impl IntoIterator<Item = String>) {
176 if self.jsonl {
177 return;
178 }
179 let mut stdout = std::io::stdout().lock();
180 for line in lines {
181 let _ = writeln!(stdout, "{line}");
182 }
183 }
184
185 pub fn warn(&self, text: &str) {
187 if self.jsonl {
188 self.line(&Record::Message {
189 level: "warning".to_string(),
190 text: text.to_string(),
191 });
192 } else {
193 eprintln!("wsb github: {text}");
194 }
195 }
196
197 pub fn fail(&self, message: &str) {
199 if self.jsonl {
200 self.line(&Record::Failure {
201 message: message.to_string(),
202 });
203 } else {
204 eprintln!("wsb github: {message}");
205 }
206 }
207}