1use serde::Serialize;
2use serde_json::Value;
3use std::fmt;
4use std::ops::Deref;
5
6#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
15pub struct Event(Value);
16
17impl Event {
18 pub fn as_value(&self) -> &Value {
20 &self.0
21 }
22
23 pub fn into_value(self) -> Value {
25 self.0
26 }
27}
28
29impl From<Event> for Value {
30 fn from(event: Event) -> Self {
31 event.0
32 }
33}
34
35impl Deref for Event {
36 type Target = Value;
37
38 fn deref(&self) -> &Self::Target {
39 &self.0
40 }
41}
42
43impl fmt::Display for Event {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 write!(f, "{}", self.0)
46 }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
56pub enum BuildError {
57 ReservedField(String),
58 NonObjectField(String),
59 EmptyRequiredField(String),
60}
61
62impl fmt::Display for BuildError {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::ReservedField(msg) => write!(f, "reserved field: {msg}"),
66 Self::NonObjectField(msg) => write!(f, "non-object field: {msg}"),
67 Self::EmptyRequiredField(msg) => write!(f, "empty required field: {msg}"),
68 }
69 }
70}
71
72impl std::error::Error for BuildError {}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
76#[serde(rename_all = "lowercase")]
77pub enum LogLevel {
78 Debug,
79 Info,
80 Warn,
81 Error,
82}
83
84impl LogLevel {
85 pub(crate) fn as_str(&self) -> &'static str {
86 match self {
87 Self::Debug => "debug",
88 Self::Info => "info",
89 Self::Warn => "warn",
90 Self::Error => "error",
91 }
92 }
93}
94
95pub struct ResultBuilder {
101 payload: Value,
102 trace: Option<Value>,
103}
104
105impl ResultBuilder {
106 pub fn trace(mut self, trace: Value) -> Self {
108 self.trace = Some(trace);
109 self
110 }
111
112 pub fn build(self) -> Result<Event, BuildError> {
114 let trace = self
115 .trace
116 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
117 let mut obj = serde_json::Map::new();
118 obj.insert("kind".to_string(), Value::String("result".to_string()));
119 obj.insert("result".to_string(), self.payload);
120 obj.insert("trace".to_string(), trace);
121 Ok(Event(Value::Object(obj)))
122 }
123}
124
125pub fn json_result(payload: Value) -> ResultBuilder {
127 ResultBuilder {
128 payload,
129 trace: None,
130 }
131}
132
133pub struct ErrorBuilder {
139 code: String,
140 message: String,
141 retryable: bool,
142 hint: Option<String>,
143 fields: serde_json::Map<String, Value>,
144 trace: Option<Value>,
145 build_error: Option<BuildError>,
146}
147
148impl ErrorBuilder {
149 pub fn retryable(mut self) -> Self {
151 self.retryable = true;
152 self
153 }
154
155 pub fn retryable_if(mut self, should_retry: bool) -> Self {
157 self.retryable = should_retry;
158 self
159 }
160
161 pub fn hint(mut self, hint: &str) -> Self {
163 self.hint = Some(hint.to_string());
164 self
165 }
166
167 pub fn hint_if_some(mut self, hint: Option<&str>) -> Self {
169 if let Some(h) = hint {
170 self.hint = Some(h.to_string());
171 }
172 self
173 }
174
175 pub fn field(mut self, name: &str, value: Value) -> Self {
177 if self.build_error.is_none() {
178 match name {
179 "code" | "message" | "hint" | "retryable" => {
180 self.build_error = Some(BuildError::ReservedField(format!(
181 "cannot write reserved field {name:?} to error payload"
182 )));
183 }
184 _ => {
185 self.fields.insert(name.to_string(), value);
186 }
187 }
188 }
189 self
190 }
191
192 pub fn fields(mut self, fields: Value) -> Self {
194 if self.build_error.is_none() {
195 match fields {
196 Value::Object(map) => {
197 for (k, v) in map {
198 match k.as_str() {
199 "code" | "message" | "hint" | "retryable" => {
200 self.build_error = Some(BuildError::ReservedField(format!(
201 "cannot write reserved field {k:?} to error payload"
202 )));
203 return self;
204 }
205 _ => {
206 self.fields.insert(k, v);
207 }
208 }
209 }
210 }
211 _ => {
212 self.build_error = Some(BuildError::NonObjectField(
213 "fields() argument must be a JSON object".to_string(),
214 ));
215 }
216 }
217 }
218 self
219 }
220
221 pub fn extend<T: Serialize>(mut self, value: T) -> Self {
223 if self.build_error.is_none() {
224 match serde_json::to_value(&value) {
225 Ok(Value::Object(map)) => {
226 for (k, v) in map {
227 match k.as_str() {
228 "code" | "message" | "hint" | "retryable" => {
229 self.build_error = Some(BuildError::ReservedField(format!(
230 "cannot write reserved field {k:?} to error payload"
231 )));
232 return self;
233 }
234 _ => {
235 self.fields.insert(k, v);
236 }
237 }
238 }
239 }
240 Ok(_) => {
241 self.build_error = Some(BuildError::NonObjectField(
242 "extend() argument must serialize to a JSON object".to_string(),
243 ));
244 }
245 Err(_) => {
246 self.build_error = Some(BuildError::NonObjectField(
247 "extend() argument serialization failed".to_string(),
248 ));
249 }
250 }
251 }
252 self
253 }
254
255 pub fn trace(mut self, trace: Value) -> Self {
257 self.trace = Some(trace);
258 self
259 }
260
261 pub fn build(self) -> Result<Event, BuildError> {
263 if let Some(err) = self.build_error {
264 return Err(err);
265 }
266
267 let mut error_obj = self.fields;
268 error_obj.insert("code".to_string(), Value::String(self.code));
269 error_obj.insert("message".to_string(), Value::String(self.message));
270 error_obj.insert("retryable".to_string(), Value::Bool(self.retryable));
271 if let Some(h) = self.hint {
272 error_obj.insert("hint".to_string(), Value::String(h));
273 }
274
275 let trace = self
276 .trace
277 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
278 let mut obj = serde_json::Map::new();
279 obj.insert("kind".to_string(), Value::String("error".to_string()));
280 obj.insert("error".to_string(), Value::Object(error_obj));
281 obj.insert("trace".to_string(), trace);
282
283 Ok(Event(Value::Object(obj)))
284 }
285}
286
287#[allow(clippy::panic)]
291pub fn json_error(code: &str, message: &str) -> ErrorBuilder {
292 if code.is_empty() {
293 panic!("json_error: code must not be empty");
294 }
295 if message.is_empty() {
296 panic!("json_error: message must not be empty");
297 }
298 ErrorBuilder {
299 code: code.to_string(),
300 message: message.to_string(),
301 retryable: false,
302 hint: None,
303 fields: serde_json::Map::new(),
304 trace: None,
305 build_error: None,
306 }
307}
308
309pub struct ProgressBuilder {
315 payload: Value,
316 trace: Option<Value>,
317}
318
319impl ProgressBuilder {
320 pub fn trace(mut self, trace: Value) -> Self {
322 self.trace = Some(trace);
323 self
324 }
325
326 pub fn build(self) -> Result<Event, BuildError> {
328 let trace = self
329 .trace
330 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
331 let mut obj = serde_json::Map::new();
332 obj.insert("kind".to_string(), Value::String("progress".to_string()));
333 obj.insert("progress".to_string(), self.payload);
334 obj.insert("trace".to_string(), trace);
335
336 Ok(Event(Value::Object(obj)))
337 }
338}
339
340pub fn json_progress(payload: Value) -> ProgressBuilder {
343 ProgressBuilder {
344 payload,
345 trace: None,
346 }
347}
348
349pub struct LogBuilder {
355 payload: Value,
356 trace: Option<Value>,
357}
358
359impl LogBuilder {
360 pub fn trace(mut self, trace: Value) -> Self {
362 self.trace = Some(trace);
363 self
364 }
365
366 pub fn build(self) -> Result<Event, BuildError> {
368 let trace = self
369 .trace
370 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
371 let mut obj = serde_json::Map::new();
372 obj.insert("kind".to_string(), Value::String("log".to_string()));
373 obj.insert("log".to_string(), self.payload);
374 obj.insert("trace".to_string(), trace);
375
376 Ok(Event(Value::Object(obj)))
377 }
378}
379
380pub fn json_log(payload: Value) -> LogBuilder {
383 LogBuilder {
384 payload,
385 trace: None,
386 }
387}
388
389#[allow(clippy::panic, clippy::expect_used)]
404pub fn build_cli_error(message: &str, hint: Option<&str>) -> Event {
405 if message.is_empty() {
406 panic!("build_cli_error: message must not be empty");
407 }
408 json_error("cli_error", message)
409 .hint_if_some(hint)
410 .build()
411 .expect("build_cli_error: builder returned error unexpectedly")
412}
413
414pub fn validate_protocol_event(event: &Value, strict: bool) -> Result<(), String> {
424 validate_protocol_event_base(event)?;
425 if strict {
426 validate_protocol_event_strict_payload(event)?;
427 }
428 Ok(())
429}
430
431fn validate_protocol_event_base(event: &Value) -> Result<(), String> {
432 let Some(obj) = event.as_object() else {
433 return Err("event must be a JSON object".to_string());
434 };
435 let Some(kind) = obj.get("kind").and_then(Value::as_str) else {
436 return Err("event.kind must be one of result, error, progress, log".to_string());
437 };
438 if !matches!(kind, "result" | "error" | "progress" | "log") {
439 return Err(format!("unsupported event kind {kind:?}"));
440 }
441 if !obj.contains_key(kind) {
442 return Err(format!("event payload field {kind:?} is required"));
443 }
444 for key in obj.keys() {
445 if key != "kind" && key != kind && key != "trace" {
446 return Err(format!("unexpected top-level field {key:?}"));
447 }
448 }
449 if let Some(trace) = obj.get("trace")
450 && !trace.is_object()
451 {
452 return Err("event.trace must be a JSON object when present".to_string());
453 }
454 if kind == "error" {
455 validate_error_payload(obj.get("error"))?;
456 }
457 Ok(())
458}
459
460fn validate_error_payload(error: Option<&Value>) -> Result<(), String> {
461 let Some(error) = error.and_then(Value::as_object) else {
462 return Err("event.error must be a JSON object".to_string());
463 };
464 match error.get("code").and_then(Value::as_str) {
465 Some(code) if !code.is_empty() => {}
466 _ => return Err("event.error.code must be a non-empty string".to_string()),
467 }
468 match error.get("message").and_then(Value::as_str) {
469 Some(message) if !message.is_empty() => {}
470 _ => return Err("event.error.message must be a non-empty string".to_string()),
471 }
472 if error.get("hint").is_some_and(|hint| !hint.is_string()) {
473 return Err("event.error.hint must be a string when present".to_string());
474 }
475 Ok(())
476}
477
478pub fn validate_protocol_stream(events: &[Value], strict: bool) -> Result<(), String> {
483 let mut terminal_kind: Option<&str> = None;
484 for (idx, event) in events.iter().enumerate() {
485 validate_protocol_event(event, strict).map_err(|err| format!("event {idx}: {err}"))?;
486 let Some(kind) = event.get("kind").and_then(Value::as_str) else {
487 return Err(format!("event {idx}: missing kind"));
488 };
489 match kind {
490 "log" | "progress" => {
491 if terminal_kind.is_some() {
492 return Err(format!("event {idx}: non-terminal event after terminal"));
493 }
494 }
495 "result" | "error" => {
496 if terminal_kind.is_some() {
497 return Err(format!("event {idx}: duplicate terminal event"));
498 }
499 terminal_kind = Some(kind);
500 }
501 _ => return Err(format!("event {idx}: unsupported event kind {kind:?}")),
502 }
503 }
504 if terminal_kind.is_none() {
505 return Err("event stream must contain exactly one terminal result or error".to_string());
506 }
507 Ok(())
508}
509
510fn validate_protocol_event_strict_payload(event: &Value) -> Result<(), String> {
514 if !event.get("trace").is_some_and(Value::is_object) {
515 return Err("event.trace is required by the strict profile".to_string());
516 }
517 match event.get("kind").and_then(Value::as_str) {
518 Some("error") => validate_strict_error_payload(event.get("error")),
519 _ => Ok(()),
520 }
521}
522
523fn validate_strict_error_payload(error: Option<&Value>) -> Result<(), String> {
524 let Some(error) = error.and_then(Value::as_object) else {
525 return Err("event.error must be a JSON object in the strict profile".to_string());
526 };
527 require_non_empty_string(error, "code", "event.error")?;
528 require_non_empty_string(error, "message", "event.error")?;
529 if error.get("retryable").and_then(Value::as_bool).is_none() {
530 return Err("event.error.retryable must be a boolean in the strict profile".to_string());
531 }
532 if error.contains_key("hint") && !error.get("hint").is_some_and(Value::is_string) {
533 return Err("event.error.hint must be a string when present".to_string());
534 }
535 Ok(())
536}
537
538fn require_non_empty_string(
539 payload: &serde_json::Map<String, Value>,
540 field: &str,
541 path: &str,
542) -> Result<(), String> {
543 if payload
544 .get(field)
545 .and_then(Value::as_str)
546 .is_some_and(|value| !value.is_empty())
547 {
548 return Ok(());
549 }
550 Err(format!(
551 "{path}.{field} must be a non-empty string in the strict profile"
552 ))
553}
554
555#[derive(Clone, Debug, PartialEq)]
561pub enum DecodedEvent {
562 Result(DecodedResult),
563 Error(DecodedError),
564 Progress(DecodedProgress),
565 Log(DecodedLog),
566}
567
568#[derive(Clone, Debug, PartialEq)]
570pub struct DecodedResult {
571 pub result: Value,
573 pub trace: Option<Value>,
575}
576
577#[derive(Clone, Debug, PartialEq)]
579pub struct DecodedError {
580 pub code: String,
581 pub message: String,
582 pub retryable: bool,
583 pub hint: Option<String>,
584 pub fields: serde_json::Map<String, Value>,
586 pub trace: Option<Value>,
588}
589
590#[derive(Clone, Debug, PartialEq)]
592pub struct DecodedProgress {
593 pub progress: Value,
595 pub trace: Option<Value>,
597}
598
599#[derive(Clone, Debug, PartialEq)]
601pub struct DecodedLog {
602 pub log: Value,
604 pub trace: Option<Value>,
606}
607
608#[derive(Clone, Debug, PartialEq, Eq)]
610pub enum EventDecodeError {
611 InvalidJson(String),
613 InvalidEvent(String),
615}
616
617impl fmt::Display for EventDecodeError {
618 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
619 match self {
620 Self::InvalidJson(err) => write!(f, "invalid JSON: {err}"),
621 Self::InvalidEvent(err) => write!(f, "invalid protocol event: {err}"),
622 }
623 }
624}
625
626impl std::error::Error for EventDecodeError {}
627
628pub fn decode_protocol_event(text: &str) -> Result<DecodedEvent, EventDecodeError> {
632 let value: Value =
633 serde_json::from_str(text).map_err(|err| EventDecodeError::InvalidJson(err.to_string()))?;
634 validate_protocol_event(&value, true).map_err(EventDecodeError::InvalidEvent)?;
635
636 let malformed = || {
639 EventDecodeError::InvalidEvent(
640 "event passed strict validation but has an unexpected shape".to_string(),
641 )
642 };
643 let obj = value.as_object().ok_or_else(malformed)?;
644 let trace = obj.get("trace").cloned();
645 match obj.get("kind").and_then(Value::as_str) {
646 Some("result") => Ok(DecodedEvent::Result(DecodedResult {
647 result: obj.get("result").cloned().unwrap_or(Value::Null),
648 trace,
649 })),
650 Some("error") => {
651 let mut fields = obj
652 .get("error")
653 .and_then(Value::as_object)
654 .ok_or_else(malformed)?
655 .clone();
656 let code = fields
657 .remove("code")
658 .and_then(|v| v.as_str().map(str::to_string))
659 .ok_or_else(malformed)?;
660 let message = fields
661 .remove("message")
662 .and_then(|v| v.as_str().map(str::to_string))
663 .ok_or_else(malformed)?;
664 let retryable = fields
665 .remove("retryable")
666 .and_then(|v| v.as_bool())
667 .ok_or_else(malformed)?;
668 let hint = fields
669 .remove("hint")
670 .and_then(|v| v.as_str().map(str::to_string));
671 Ok(DecodedEvent::Error(DecodedError {
672 code,
673 message,
674 retryable,
675 hint,
676 fields,
677 trace,
678 }))
679 }
680 Some("progress") => Ok(DecodedEvent::Progress(DecodedProgress {
681 progress: obj.get("progress").cloned().unwrap_or(Value::Null),
682 trace,
683 })),
684 Some("log") => Ok(DecodedEvent::Log(DecodedLog {
685 log: obj.get("log").cloned().unwrap_or(Value::Null),
686 trace,
687 })),
688 _ => Err(malformed()),
689 }
690}